Skip to content

Instantly share code, notes, and snippets.

@lpil
Created April 13, 2017 11:21
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lpil/2b87eebf479c43eda7e4bea21587a7fb to your computer and use it in GitHub Desktop.
Save lpil/2b87eebf479c43eda7e4bea21587a7fb to your computer and use it in GitHub Desktop.
Ruby Result Monad
require 'result/ok'
require 'result/error'
#
# A generic representation of success and failure.
#
# Styled after the Result monad of Elm and Rust
# (or the Either monad of Haskell).
#
# The `#and_then` method can be used to chain functions that
# operate on the data held by a result.
#
module Result
def self.ok(value)
Ok.new(value)
end
def self.error(error)
Error.new(error)
end
end
module Result
#
# Abstract base class of Result::Ok and Result::Error
#
class Base
%i(initialize and_then as_json success?).each do |method_name|
define_method(method_name) do |*_args|
raise NotImplementedError,
"called #{method_name} on abstract class Result::Base"
end
end
#
# Call an object or lambda depending on whether the Result
# is Ok or an Error.
#
def match(ok:, error:)
if success?
ok.call(value)
else
error.call(self.error)
end
end
end
end
require 'result/base'
module Result
#
# Fail type.
# Similar to `Err` of Rust/Elm's Result monad.
#
class Error < Base
attr_reader :error
def initialize(error)
@error = error
freeze
end
def success?
false
end
def and_then
self
end
def ==(other)
self.class == other.class && error == other.error
end
end
end
require 'result/base'
module Result
#
# Success type.
# Similar to `Ok` of Rust/Elm's Result monad.
#
class Ok < Base
attr_reader :value
def initialize(value)
@value = value
freeze
end
def success?
true
end
def and_then
yield value
end
def ==(other)
self.class == other.class && value == other.value
end
end
end
require 'result'
RSpec.describe Result do
describe '.ok' do
it 'constructs an Ok value' do
res = described_class.ok('thingy')
expect(res).to be_success
expect(res.value).to eq('thingy')
end
end
describe '.error' do
it 'constructs an Error value' do
res = described_class.error('some errors')
expect(res).not_to be_success
expect(res.error).to eq('some errors')
end
end
describe '#and_then' do
it 'chains for Ok results' do
result = described_class
.ok(1)
.and_then { |e| described_class.ok(e + 1) }
.and_then { |e| described_class.ok(e * 2) }
expect(result).to be_success
expect(result.value).to eq(4)
end
it 'short circuits for Error results' do
result = described_class
.ok(1)
.and_then { |e| described_class.ok(e + 1) }
.and_then { |_e| described_class.error('an error') }
.and_then { |_e| raise 'This should never run' }
expect(result).not_to be_success
expect(result.error).to eq('an error')
end
end
describe '#match' do
it 'calls the ok path when Ok' do
res = described_class.ok(50).match(
ok: ->(x) { x + 10 },
error: ->(_) { raise 'This should never run' }
)
expect(res).to eq(60)
end
it 'calls the error path when Error' do
res = described_class.error([1, 2]).match(
ok: ->(_) { raise 'This should never run' },
error: ->(x) { x.reverse }
)
expect(res).to eq([2, 1])
end
end
describe '#==' do
it 'can be equal for Ok values' do
expect(Result.ok(1)).to eq Result.ok(1)
end
it 'can be equal for Error values' do
expect(Result.error({})).to eq Result.error({})
end
it 'can be unequal for Ok values' do
expect(Result.ok(:hi)).not_to eq Result.ok(:bi)
end
it 'can be unequal for Error values' do
expect(Result.error('a')).not_to eq Result.error('A')
end
it 'is unequal for Ok == Error values' do
expect(Result.ok(100)).not_to eq Result.error(100)
end
it 'is unequal for Error == Ok values' do
expect(Result.error(200)).not_to eq Result.ok(200)
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment