diff --git a/README.md b/README.md index 983db44..96117a6 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,8 @@ Maybe monad is a programming pattern that allows to treat nil values that same w The implementation includes three different classes: `Maybe`, `Some` and `None`. `Some` represents a value, `None` represents a non-value and `Maybe` is a constructor, which results either `Some`, or `None`. ```ruby -Maybe("I'm a value") => # -Maybe(nil) => # +Maybe("I'm a value") => # +Maybe(nil) => # ``` Both `Some` and `None` implement four trivial methods: `is_some?`, `is_none?`, `get` and `or_else` @@ -51,8 +51,8 @@ In addition, `Some` and `None` implement `Enumerable`, so all methods available ```ruby Maybe("Print me!").each { |v| puts v } => it puts "Print me!" Maybe(nil).each { |v| puts v } => puts nothing -Maybe(4).map { |v| Math.sqrt(v) } => # -Maybe(nil).map { |v| Math.sqrt(v) } => # +Maybe(4).map { |v| Math.sqrt(v) } => # +Maybe(nil).map { |v| Math.sqrt(v) } => # Maybe(2).inject(3) { |a, b| a + b } => 5 None().inject(3) { |a, b| a + b } => 3 ``` @@ -60,10 +60,60 @@ None().inject(3) { |a, b| a + b } => 3 All the other methods you call on `Some` are forwarded to the `value`. ```ruby -Maybe("I'm a value").upcase => # +Maybe("I'm a value").upcase => # Maybe(nil).upcase => None ``` +### Case expression + +Maybe implements threequals method `#===`, so it can be used in case expressions: + +```ruby +value = Maybe([nil, 1, 2, 3, 4, 5, 6].sample) + +case value +when Some + puts "Got Some: #{value.get}" +when None + puts "Got None" +end +``` + +If the type of Maybe is Some, you can also match the value: + +```ruby +value = Maybe([nil, 0, 1, 2, 3, 4, 5, 6].sample) + +case value +when Some(0) + puts "Got zero" +when Some((1..3)) + puts "Got a low number: #{value.get}" +when Some((4..6)) + puts "Got a high number: #{value.get}" +when None + puts "Got nothing" +end +``` + +For more complicated matching you can use Procs and lambdas. Proc class aliases #=== to the #call method. In practice this means that you can use Procs and lambdas in case expressions. It works also nicely with Maybe: + +```ruby +even? = ->(a) { a % 2 == 0 } +odd? = ->(a) { a % 2 != 0 } + +value = Maybe([nil, 1, 2, 3, 4, 5, 6].sample) + +case value +when Some(even?) + puts "Got even value: #{value.get}" +when Some(odd?) + puts "Got odd value: #{value.get}" +when None + puts "Got None" +end +``` + ## Examples Instead of using if-clauses to define whether a value is a `nil`, you can wrap the value with `Maybe()` and threat it the same way whether or not it is a `nil` diff --git a/lib/possibly.rb b/lib/possibly.rb index fbd9681..a0287a6 100644 --- a/lib/possibly.rb +++ b/lib/possibly.rb @@ -1,113 +1,111 @@ -module Maybe - # Parent class for Maybe::Some and Maybe::None. You should never - # instantiate this class. - class Maybe - ([:each] + Enumerable.instance_methods).each do |enumerable_method| - define_method(enumerable_method) do |*args, &block| - res = __enumerable_value.send(enumerable_method, *args, &block) - res.respond_to?(:each) ? Maybe(res.first) : res - end +class Maybe + ([:each] + Enumerable.instance_methods).each do |enumerable_method| + define_method(enumerable_method) do |*args, &block| + res = __enumerable_value.send(enumerable_method, *args, &block) + res.respond_to?(:each) ? Maybe(res.first) : res end + end - def to_ary - __enumerable_value - end - alias_method :to_a, :to_ary + def to_ary + __enumerable_value + end + alias_method :to_a, :to_ary - def ==(other) - other.class == self.class - end - alias_method :eql?, :== + def ==(other) + other.class == self.class end + alias_method :eql?, :== +end - # Represents a non-empty value - class Some < Maybe - def initialize(value) - @value = value - end +# Represents a non-empty value +class Some < Maybe + def initialize(value) + @value = value + end - def get - @value - end + def get + @value + end - def or_else(*) - @value - end + def or_else(*) + @value + end - # rubocop:disable PredicateName - def is_some? - true - end + # rubocop:disable PredicateName + def is_some? + true + end - def is_none? - false - end - # rubocop:enable PredicateName + def is_none? + false + end + # rubocop:enable PredicateName - def ==(other) - super && get == other.get - end - alias_method :eql?, :== + def ==(other) + super && get == other.get + end + alias_method :eql?, :== - def method_missing(method_sym, *args, &block) - map { |value| value.send(method_sym, *args, &block) } - end + def ===(other) + other && other.class == self.class && @value === other.get + end - private + def method_missing(method_sym, *args, &block) + map { |value| value.send(method_sym, *args, &block) } + end - def __enumerable_value - [@value] - end + private + + def __enumerable_value + [@value] end +end - # Represents an empty value - class None < Maybe - def get - fail 'No such element' - end +# Represents an empty value +class None < Maybe + def get + fail 'No such element' + end - def or_else(els = nil) - block_given? ? yield : els - end + def or_else(els = nil) + block_given? ? yield : els + end - # rubocop:disable PredicateName - def is_some? - false - end + # rubocop:disable PredicateName + def is_some? + false + end - def is_none? - true - end - # rubocop:enable PredicateName + def is_none? + true + end + # rubocop:enable PredicateName - def method_missing(*) - None.new - end + def method_missing(*) + self + end - private + private - def __enumerable_value - [] - end + def __enumerable_value + [] end end # rubocop:disable MethodName def Maybe(value) if value.nil? || (value.respond_to?(:length) && value.length == 0) - None + None() else Some(value) end end def Some(value) - Maybe::Some.new(value) + Some.new(value) end def None - Maybe::None.new + None.new end - -None = None() # rubocop:enable MethodName diff --git a/spec/spec.rb b/spec/spec.rb index 2c0a533..bc7709e 100644 --- a/spec/spec.rb +++ b/spec/spec.rb @@ -4,18 +4,18 @@ describe "enumerable" do it "#each" do expect { |b| Some(1).each(&b) }.to yield_with_args(1) - expect { |b| None.each(&b) }.not_to yield_with_args + expect { |b| None().each(&b) }.not_to yield_with_args end it "#map" do expect(Some(2).map { |v| v * v }.get).to eql(4) - expect { |b| None.map(&b) }.not_to yield_with_args + expect { |b| None().map(&b) }.not_to yield_with_args end it "#inject" do expect(Some(2).inject(5) { |v| v * v }).to eql(25) - expect { |b| None.inject(&b) }.not_to yield_with_args - expect(None.inject(5) { }).to eql(5) + expect { |b| None().inject(&b) }.not_to yield_with_args + expect(None().inject(5) { }).to eql(5) end it "#select" do @@ -32,7 +32,7 @@ end } expect(Maybe(5).flat_map { |x| div.call(1, x) }).to eql(Maybe(0.2)) - expect(Maybe(0).flat_map { |x| div.call(1, x) }).to eql(None) + expect(Maybe(0).flat_map { |x| div.call(1, x) }).to eql(None()) end end @@ -53,12 +53,12 @@ describe "is_a" do it "Some" do - expect(Some(1).is_a?(Maybe::Some)).to eql(true) - expect(Some(1).is_a?(Maybe::None)).to eql(false) - expect(None.is_a?(Maybe::Some)).to eql(false) - expect(None.is_a?(Maybe::None)).to eql(true) - expect(Some(1).is_a?(Maybe::Maybe)).to eql(true) - expect(None.is_a?(Maybe::Maybe)).to eql(true) + expect(Some(1).is_a?(Some)).to eql(true) + expect(Some(1).is_a?(None)).to eql(false) + expect(None().is_a?(Some)).to eql(false) + expect(None().is_a?(None)).to eql(true) + expect(Some(1).is_a?(Maybe)).to eql(true) + expect(None().is_a?(Maybe)).to eql(true) end end @@ -71,6 +71,58 @@ end end + describe "case equality" do + it "#===" do + expect(Some(1) === Some(1)).to be_true + expect(Maybe(1) === Some(2)).to be_false + expect(Some(1) === None).to be_false + expect(None === Some(1)).to be_false + expect(None === None()).to be_true + expect(Some((1..3)) === Some(2)).to be_true + expect(Some(Integer) === Some(2)).to be_true + expect(Maybe === Some(2)).to be_true + expect(Maybe === None()).to be_true + expect(Some === Some(6)).to be_true + end + end + + describe "case expression" do + def test_case_when(case_value, match_value, non_match_value) + value = case case_value + when non_match_value + false + when match_value + true + else + false + end + + expect(value).to be_true + end + + it "matches Some" do + test_case_when(Maybe(1), Some, None) + end + + it "matches None" do + test_case_when(Maybe(nil), None, Some) + end + + it "matches to integer value" do + test_case_when(Maybe(1), Some(1), Some(2)) + end + + it "matches to range" do + test_case_when(Maybe(1), Some((0..2)), Some((2..3))) + end + + it "matches to lambda" do + even = ->(a) { a % 2 == 0 } + odd = ->(a) { a % 2 == 1 } + test_case_when(Maybe(2), Some(even), Some(odd)) + end + end + describe "to array" do it "#to_ary" do a, _ = Maybe(1) @@ -91,8 +143,8 @@ end it "or_else" do - expect(None.or_else(true)).to eql(true) - expect(None.or_else { false }).to eql(false) + expect(None().or_else(true)).to eql(true) + expect(None().or_else { false }).to eql(false) expect(Some(1).or_else(2)).to eql(1) expect(Some(1).or_else { 2 }).to eql(1) end