From 5c0bd2f43299f47e0631fd169ffe4816e150f1d0 Mon Sep 17 00:00:00 2001 From: Mikko Koski Date: Tue, 5 Aug 2014 22:12:17 +0300 Subject: [PATCH 01/12] Pass method call directly to inner value --- lib/possibly.rb | 16 ++++++++++++++++ spec/spec.rb | 8 ++++++++ 2 files changed, 24 insertions(+) diff --git a/lib/possibly.rb b/lib/possibly.rb index a0287a6..8b5ccfd 100644 --- a/lib/possibly.rb +++ b/lib/possibly.rb @@ -19,6 +19,18 @@ def ==(other) # Represents a non-empty value class Some < Maybe + + class SomeInnerValue + + def initialize(value) + @value = value + end + + def method_missing(method_sym, *args, &block) + Maybe(@value.send(method_sym, *args, &block)) + end + end + def initialize(value) @value = value end @@ -54,6 +66,10 @@ def method_missing(method_sym, *args, &block) map { |value| value.send(method_sym, *args, &block) } end + def inner + SomeInnerValue.new(@value) + end + private def __enumerable_value diff --git a/spec/spec.rb b/spec/spec.rb index bc7709e..9332309 100644 --- a/spec/spec.rb +++ b/spec/spec.rb @@ -156,4 +156,12 @@ def test_case_when(case_value, match_value, non_match_value) expect(Some([1, 2, 3]).map { |arr| arr.map { |v| v * v } }.get).to eql([1, 4, 9]) end end + + describe "inner" do + it "forwards all (also Enumerable) methods" do + expect(Some(["first", "second", "third"]).inner.first.upcase.or_else { false }).to eql("FIRST") + expect(Some(["first", "second", "third"]).inner.map(&:upcase).or_else { false }).to eql(["FIRST", "SECOND", "THIRD"]) + expect(Some([]).inner.first.upcase.or_else { "NONE" }).to eql("NONE") + end + end end \ No newline at end of file From a8cd46f3ad32fc8632313c3f675d91f82e9b7cde Mon Sep 17 00:00:00 2001 From: Mikko Koski Date: Thu, 14 Aug 2014 23:18:02 +0300 Subject: [PATCH 02/12] Tests for combine method --- spec/spec.rb | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/spec/spec.rb b/spec/spec.rb index 4c5c86b..e62c02b 100644 --- a/spec/spec.rb +++ b/spec/spec.rb @@ -156,4 +156,24 @@ def test_case_when(case_value, match_value, non_match_value) expect(Some([1, 2, 3]).map { |arr| arr.map { |v| v * v } }.get).to eql([1, 4, 9]) end end + + describe "combine" do + it "combines multiple Some values" do + expect(Maybe("foo").combine(Maybe("bar")).get).to eql(["foo", "bar"]) + expect(Maybe("foo").combine(Maybe("bar"), Maybe("baz")).get).to eql(["foo", "bar", "baz"]) + expect(Maybe.combine(Maybe("foo"), Maybe("bar"), Maybe("baz")).get).to eql(["foo", "bar", "baz"]) + end + + it "combines with block" do + expect(Maybe("foo").combine(Maybe("bar")) { |a, b| "#{a} + #{b}" }.get).to eql("foo + bar") + expect(Maybe.combine(Maybe("foo"), Maybe("bar")) { |a, b| "#{a} + #{b}" }.get).to eql("foo + bar") + end + + it "returns None if any None" do + expect(Maybe("foo").combine(Maybe(nil)).or_else { false }).to eql(false) + expect(Maybe("foo").combine(Maybe(nil)) { |a, b| "#{a} + #{b}" }.or_else { false }).to eql(false) + expect(Maybe.combine(Maybe("foo"), Maybe(nil)).or_else { false }).to eql(false) + expect(Maybe.combine(Maybe("foo"), Maybe(nil)) { |a, b| "#{a} + #{b}" }.or_else { false }).to eql(false) + end + end end \ No newline at end of file From 6adacaf7c42780a116b9723a2129615d7257f8af Mon Sep 17 00:00:00 2001 From: Mikko Koski Date: Thu, 21 Aug 2014 22:13:17 +0300 Subject: [PATCH 03/12] Laziness to README --- README.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/README.md b/README.md index 2e1fcc1..7735b03 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,56 @@ when None end ``` +## Laziness + +Ruby 2.0 introduced lazy enumerables. By calling `lazy` on any enumerable, you get the lazy version of it. Same goes with Maybe. + +```ruby + +called = false +m = Maybe(2).lazy.map do |value| + called = true; + value * value; +end + +puts called # => false +puts m.get # => 4 # Map is called now +puts called # => true +``` + +You can also initialize Maybe lazily by giving it a block. + +```ruby +init_called = false +map_called = false + +m = Maybe do + init_called = true + do_some_expensive_calculation # returns 1234567890 +end.map do |value| + map_called = true; + "the value of expensive calculation: #{value}"; +end + +puts init_called # => false +puts map_called # => false +puts m.get # => "the value of expensive calculation: 1234567890 # Map is called now +puts init_called # => true +puts map_called # => true +``` + +Note that if you initialize a maybe non-lazily and inspect it, you see from the class that it is a Some: + +```ruby +Maybe("I'm not lazy") => # +``` + +However, if you initialize Maybe lazily, we do not know the type before the lazy block is evaluated. Thus, you see a different output when printing the value + +```ruby +Maybe { "I'm lazy" } => #:each>>> +``` + ## 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` From 572c7d5ffdd943d6bf3df046451e0838c3d4c08e Mon Sep 17 00:00:00 2001 From: Mikko Koski Date: Thu, 21 Aug 2014 23:02:38 +0300 Subject: [PATCH 04/12] Implement laziness --- lib/possibly.rb | 71 +++++++++++++++++++++++++++++++++++++++++++++---- spec/spec.rb | 38 ++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 5 deletions(-) diff --git a/lib/possibly.rb b/lib/possibly.rb index a0287a6..ed96021 100644 --- a/lib/possibly.rb +++ b/lib/possibly.rb @@ -2,10 +2,14 @@ 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 + res.respond_to?(:each) ? rewrap(res) : res end end + def initialize(lazy_enumerable) + @lazy = lazy_enumerable + end + def to_ary __enumerable_value end @@ -15,6 +19,53 @@ def ==(other) other.class == self.class end alias_method :eql?, :== + + def get + __evaluated.get + end + + def or_else(*args) + __evaluated.or_else(*args) + end + + # rubocop:disable PredicateName + def is_some? + __evaluated.is_some? + end + + def is_none? + __evaluated.is_none? + end + # rubocop:enable PredicateName + + def lazy + Maybe.new(__enumerable_value.lazy) + end + + private + + def __enumerable_value + @lazy + end + + def __evaluated + @evaluated ||= Maybe(@lazy.first) + end + + def rewrap(enumerable) + Maybe.new(enumerable) + end + + def self.from_block(&block) + Maybe.new(lazy_enum_from_block(&block)) + end + + def self.lazy_enum_from_block(&block) + Enumerator.new do |yielder| + yielder << block.call + end.lazy + end + end # Represents a non-empty value @@ -59,10 +110,16 @@ def method_missing(method_sym, *args, &block) def __enumerable_value [@value] end + + def rewrap(enumerable) + Maybe(enumerable.first) + end end # Represents an empty value class None < Maybe + def initialize; end + def get fail 'No such element' end @@ -93,11 +150,15 @@ def __enumerable_value end # rubocop:disable MethodName -def Maybe(value) - if value.nil? || (value.respond_to?(:length) && value.length == 0) - None() +def Maybe(value = nil, &block) + if block + Maybe.from_block(&block) else - Some(value) + if value.nil? || (value.respond_to?(:length) && value.length == 0) + None() + else + Some(value) + end end end diff --git a/spec/spec.rb b/spec/spec.rb index 4c5c86b..4285913 100644 --- a/spec/spec.rb +++ b/spec/spec.rb @@ -156,4 +156,42 @@ def test_case_when(case_value, match_value, non_match_value) expect(Some([1, 2, 3]).map { |arr| arr.map { |v| v * v } }.get).to eql([1, 4, 9]) end end + + describe "laziness" do + it "can be initialized lazily" do + init_called = false + map_called = false + + m = Maybe do + init_called = true + 2 + end.map do |v| + map_called = true + v * v + end + + expect(init_called).to eql(false) + expect(map_called).to eql(false) + expect(m.get).to eql(4) + expect(init_called).to eql(true) + expect(map_called).to eql(true) + end + + it "can be converted to lazy" do + map_called = false + + m = Maybe(2).lazy.map do |v| + map_called = true + v * v + end + + expect(map_called).to eql(false) + expect(m.get).to eql(4) + expect(map_called).to eql(true) + end + end + + def factors(num) + Maybe((2..num - 1).select { |n| num % n == 0 }) + end end \ No newline at end of file From 6a14bb37c0c474236d4c8725d03e9be406223145 Mon Sep 17 00:00:00 2001 From: Mikko Koski Date: Thu, 21 Aug 2014 23:26:40 +0300 Subject: [PATCH 05/12] Case expression with lazy Maybes --- lib/possibly.rb | 10 +++++++++- spec/spec.rb | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/lib/possibly.rb b/lib/possibly.rb index ed96021..346bb51 100644 --- a/lib/possibly.rb +++ b/lib/possibly.rb @@ -98,7 +98,11 @@ def ==(other) alias_method :eql?, :== def ===(other) - other && other.class == self.class && @value === other.get + other && (other.class == self.class || other.class == Maybe) && @value === other.get + end + + def self.===(other) + super || (other.class == Maybe && other.is_some?) end def method_missing(method_sym, *args, &block) @@ -138,6 +142,10 @@ def is_none? end # rubocop:enable PredicateName + def self.===(other) + super || (other.class == Maybe && other.is_none?) + end + def method_missing(*) self end diff --git a/spec/spec.rb b/spec/spec.rb index 4285913..7b09c38 100644 --- a/spec/spec.rb +++ b/spec/spec.rb @@ -83,6 +83,16 @@ expect(Maybe === Some(2)).to be true expect(Maybe === None()).to be true expect(Some === Some(6)).to be true + + expect(Some(1) === Some(1).lazy).to be true + expect(Maybe(1) === Some(2).lazy).to be false + expect(None === Some(1).lazy).to be false + expect(None === None().lazy).to be true + expect(Some((1..3)) === Some(2).lazy).to be true + expect(Some(Integer) === Some(2).lazy).to be true + expect(Maybe === Some(2).lazy).to be true + expect(Maybe === None().lazy).to be true + expect(Some === Some(6).lazy).to be true end end @@ -121,6 +131,10 @@ def test_case_when(case_value, match_value, non_match_value) odd = ->(a) { a % 2 == 1 } test_case_when(Maybe(2), Some(even), Some(odd)) end + + it "matches to lazy" do + test_case_when(Maybe { 1 }, Some(1), Some(2)) + end end describe "to array" do From f1b48f769359336d731598502c0809deb675e59c Mon Sep 17 00:00:00 2001 From: Mikko Koski Date: Sat, 23 Aug 2014 22:38:22 +0300 Subject: [PATCH 06/12] Make tests to pass with 1.9.3 --- README.md | 2 ++ lib/possibly.rb | 9 +++++++-- spec/spec.rb | 12 ++++++------ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 7735b03..862ae2a 100644 --- a/README.md +++ b/README.md @@ -166,6 +166,8 @@ However, if you initialize Maybe lazily, we do not know the type before the lazy Maybe { "I'm lazy" } => #:each>>> ``` +This feature needs Ruby version >= 2.0.0. + ## 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 346bb51..00e479d 100644 --- a/lib/possibly.rb +++ b/lib/possibly.rb @@ -39,7 +39,11 @@ def is_none? # rubocop:enable PredicateName def lazy - Maybe.new(__enumerable_value.lazy) + if [].respond_to?(:lazy) + Maybe.new(__enumerable_value.lazy) + else + self + end end private @@ -159,9 +163,10 @@ def __enumerable_value # rubocop:disable MethodName def Maybe(value = nil, &block) - if block + if block && [].respond_to?(:lazy) Maybe.from_block(&block) else + value = block.call if block if value.nil? || (value.respond_to?(:length) && value.length == 0) None() else diff --git a/spec/spec.rb b/spec/spec.rb index 7b09c38..a7833fa 100644 --- a/spec/spec.rb +++ b/spec/spec.rb @@ -184,8 +184,8 @@ def test_case_when(case_value, match_value, non_match_value) v * v end - expect(init_called).to eql(false) - expect(map_called).to eql(false) + expect(init_called).to eql(lazy_enabled ? false : true) + expect(map_called).to eql(lazy_enabled ? false : true) expect(m.get).to eql(4) expect(init_called).to eql(true) expect(map_called).to eql(true) @@ -199,13 +199,13 @@ def test_case_when(case_value, match_value, non_match_value) v * v end - expect(map_called).to eql(false) + expect(map_called).to eql(lazy_enabled ? false : true) expect(m.get).to eql(4) expect(map_called).to eql(true) end end - def factors(num) - Maybe((2..num - 1).select { |n| num % n == 0 }) + def lazy_enabled + @lazy_enabled ||= [].respond_to?(:lazy) end -end \ No newline at end of file +end From cad21af42eff923235b740b453917c081f52d788 Mon Sep 17 00:00:00 2001 From: Mikko Koski Date: Sat, 23 Aug 2014 22:56:36 +0300 Subject: [PATCH 07/12] Breaking change: Rename or_else to get_or_else --- README.md | 24 ++++++++++++------------ lib/possibly.rb | 4 ++-- spec/spec.rb | 14 +++++++------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 2e1fcc1..5956896 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Maybe monad implementation for Ruby ```ruby -puts Maybe(User.find_by_id("123")).username.downcase.or_else { "N/A" } +puts Maybe(User.find_by_id("123")).username.downcase.get_or_else { "N/A" } => # puts downcased username if user "123" can be found, otherwise puts "N/A" ``` @@ -21,7 +21,7 @@ gem install possibly ``` require 'possibly' -first_name = Maybe(deep_hash)[:account][:profile][:first_name].or_else { "No first name available" } +first_name = Maybe(deep_hash)[:account][:profile][:first_name].get_or_else { "No first name available" } ``` ## Documentation @@ -38,14 +38,14 @@ Maybe(nil) => # Both `Some` and `None` implement four trivial methods: `is_some?`, `is_none?`, `get` and `or_else` ```ruby -Maybe("I'm a value").is_some? => true -Maybe("I'm a value").is_none? => false -Maybe(nil).is_some? => false -Maybe(nil).is_none? => true -Maybe("I'm a value").get => "I'm a value" -Maybe("I'm a value").or_else { "No value" } => "I'm a value" -Maybe(nil).get => RuntimeError: No such element -Maybe(nil).or_else { "No value" } => "No value" +Maybe("I'm a value").is_some? => true +Maybe("I'm a value").is_none? => false +Maybe(nil).is_some? => false +Maybe(nil).is_none? => true +Maybe("I'm a value").get => "I'm a value" +Maybe("I'm a value").get_or_else { "No value" } => "I'm a value" +Maybe(nil).get => RuntimeError: No such element +Maybe(nil).get_or_else { "No value" } => "No value" ``` In addition, `Some` and `None` implement `Enumerable`, so all methods available for `Enumerable` are available for `Some` and `None`: @@ -134,7 +134,7 @@ end With Maybe(): ```ruby -number_of_friends = Maybe(User.find_by_id(user_id)).friends.count.or_else { 0 } +number_of_friends = Maybe(User.find_by_id(user_id)).friends.count.get_or_else { 0 } ``` Same in HAML view, without Maybe(): @@ -147,7 +147,7 @@ Same in HAML view, without Maybe(): ``` ```haml -= Maybe(@user).friends.count.or_else { 0 } += Maybe(@user).friends.count.get_or_else { 0 } ``` ## Tests diff --git a/lib/possibly.rb b/lib/possibly.rb index a0287a6..b52bdb6 100644 --- a/lib/possibly.rb +++ b/lib/possibly.rb @@ -27,7 +27,7 @@ def get @value end - def or_else(*) + def get_or_else(*) @value end @@ -67,7 +67,7 @@ def get fail 'No such element' end - def or_else(els = nil) + def get_or_else(els = nil) block_given? ? yield : els end diff --git a/spec/spec.rb b/spec/spec.rb index 4c5c86b..198c5dd 100644 --- a/spec/spec.rb +++ b/spec/spec.rb @@ -136,17 +136,17 @@ def test_case_when(case_value, match_value, non_match_value) end end - describe "get and or_else" do + describe "get and get_or_else" do it "get" do expect { None.get }.to raise_error expect(Some(1).get).to eql(1) end - it "or_else" do - 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) + it "get_or_else" do + expect(None().get_or_else(true)).to eql(true) + expect(None().get_or_else { false }).to eql(false) + expect(Some(1).get_or_else(2)).to eql(1) + expect(Some(1).get_or_else { 2 }).to eql(1) end end @@ -156,4 +156,4 @@ def test_case_when(case_value, match_value, non_match_value) expect(Some([1, 2, 3]).map { |arr| arr.map { |v| v * v } }.get).to eql([1, 4, 9]) end end -end \ No newline at end of file +end From cef5b0e0fa4987262b1b20ce48feddd45e799c9d Mon Sep 17 00:00:00 2001 From: Mikko Koski Date: Sat, 23 Aug 2014 23:41:39 +0300 Subject: [PATCH 08/12] or_else examples --- README.md | 20 ++++++++++++++++++++ lib/possibly.rb | 8 ++++++++ spec/spec.rb | 23 +++++++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/README.md b/README.md index 5956896..5fbe933 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,26 @@ when None end ``` +## or_else + +`or_else` returns the current `Maybe` if it's a `Some`, but if it's a `None`, it returns the parameter that was given to it (which should be a `Maybe`). + +Here's an example: Show "title", which is person's job title or degree if she doesn't have a job or "Unknown" if both are missing. + +```ruby +maybe_person = Maybe(person) + +title = maybe_person.job.title.or_else { maybe_person.degree }.get_or_else { "Unknown" } + +title = if person && person.job && person.job.title.present? + person.job.title +elsif person && person.degree.present? + person.degree +else + "Unknown" +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 b52bdb6..ba19b49 100644 --- a/lib/possibly.rb +++ b/lib/possibly.rb @@ -31,6 +31,10 @@ def get_or_else(*) @value end + def or_else(*) + self + end + # rubocop:disable PredicateName def is_some? true @@ -71,6 +75,10 @@ def get_or_else(els = nil) block_given? ? yield : els end + def or_else(els = nil, &block) + block ? block.call : els + end + # rubocop:disable PredicateName def is_some? false diff --git a/spec/spec.rb b/spec/spec.rb index 198c5dd..2d8b2cb 100644 --- a/spec/spec.rb +++ b/spec/spec.rb @@ -150,6 +150,29 @@ def test_case_when(case_value, match_value, non_match_value) end end + describe "or_else" do + it "returns self if it's a Some" do + current = Maybe(true) + other = Maybe(true) + + expect(current.or_else(other)).to equal current + end + + it "returns other if it's a Some" do + current = Maybe(nil) + other = Maybe(true) + + expect(current.or_else(other)).to equal other + end + + it "takes also a block" do + current = Maybe(true) + other = Maybe(true) + + expect(current.or_else { other }).to equal current + end + end + describe "forward" do it "forwards methods" do expect(Some("maybe").upcase.get).to eql("MAYBE") From ba4de84108286ee1a71babc33ed7d39739ccd08a Mon Sep 17 00:00:00 2001 From: Mikko Koski Date: Sat, 16 Aug 2014 00:19:05 +0300 Subject: [PATCH 09/12] Implement combine --- README.md | 13 +++++++++++++ lib/possibly.rb | 13 +++++++++++++ spec/spec.rb | 10 ++++------ 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2e1fcc1..f961232 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,19 @@ when None end ``` +## `combine([maybes])` + +With `combine` you can create a new `Maybe` which includes an array of values from combined `Maybe`s. If any of the combined `Maybe`s is a `None`, a `None` will be returned. + +``` +mparams = Maybe(params) + +duration = Maybe + .combine(mparams[:start_date], mparams[:end_date]) + .map { |(start, end)| Date.parse(end) - Date.parse(start) } + .get_or_else "Unknown" +``` + ## 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 a0287a6..000f8d0 100644 --- a/lib/possibly.rb +++ b/lib/possibly.rb @@ -15,6 +15,19 @@ def ==(other) other.class == self.class end alias_method :eql?, :== + + def combine(*maybes) + Maybe.combine(self, *maybes) + end + + def self.combine(*maybes) + if maybes.any?(&:is_none?) + None() + else + values = maybes.map(&:get) + Maybe(values) + end + end end # Represents a non-empty value diff --git a/spec/spec.rb b/spec/spec.rb index e62c02b..c572f54 100644 --- a/spec/spec.rb +++ b/spec/spec.rb @@ -162,11 +162,9 @@ def test_case_when(case_value, match_value, non_match_value) expect(Maybe("foo").combine(Maybe("bar")).get).to eql(["foo", "bar"]) expect(Maybe("foo").combine(Maybe("bar"), Maybe("baz")).get).to eql(["foo", "bar", "baz"]) expect(Maybe.combine(Maybe("foo"), Maybe("bar"), Maybe("baz")).get).to eql(["foo", "bar", "baz"]) - end - - it "combines with block" do - expect(Maybe("foo").combine(Maybe("bar")) { |a, b| "#{a} + #{b}" }.get).to eql("foo + bar") - expect(Maybe.combine(Maybe("foo"), Maybe("bar")) { |a, b| "#{a} + #{b}" }.get).to eql("foo + bar") + expect(Maybe.combine(Maybe("foo"), Maybe("bar"), Maybe("baz")).map do |(foo, bar, baz)| + "#{foo} and #{bar} and finally, #{baz}" + end.get).to eql ("foo and bar and finally, baz") end it "returns None if any None" do @@ -176,4 +174,4 @@ def test_case_when(case_value, match_value, non_match_value) expect(Maybe.combine(Maybe("foo"), Maybe(nil)) { |a, b| "#{a} + #{b}" }.or_else { false }).to eql(false) end end -end \ No newline at end of file +end From 98f5edd1a7a9ac8fbaa3f165c0d2c6a9ea3e3605 Mon Sep 17 00:00:00 2001 From: Mikko Koski Date: Sun, 24 Aug 2014 00:25:08 +0300 Subject: [PATCH 10/12] Merge conflict fix --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 5564b47..428796c 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,6 @@ when None end ``` -<<<<<<< HEAD ## or_else `or_else` returns the current `Maybe` if it's a `Some`, but if it's a `None`, it returns the parameter that was given to it (which should be a `Maybe`). @@ -149,7 +148,6 @@ duration = Maybe .get_or_else "Unknown" ``` -======= ## Laziness Ruby 2.0 introduced lazy enumerables. By calling `lazy` on any enumerable, you get the lazy version of it. Same goes with Maybe. @@ -202,7 +200,6 @@ Maybe { "I'm lazy" } => #= 2.0.0. ->>>>>>> lazy ## 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` From c598058e481a3aaa7c60002741584a54983c4b70 Mon Sep 17 00:00:00 2001 From: Mikko Koski Date: Sun, 24 Aug 2014 01:12:51 +0300 Subject: [PATCH 11/12] Lazy combine --- lib/possibly.rb | 30 ++++++++++++++++++++++-------- spec/spec.rb | 16 ++++++++++++++++ 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/lib/possibly.rb b/lib/possibly.rb index 1d1ba7c..fd4889e 100644 --- a/lib/possibly.rb +++ b/lib/possibly.rb @@ -46,17 +46,19 @@ def lazy end end - def combine(*maybes) - Maybe.combine(self, *maybes) + def combine(*lazys) + Maybe.from_block { + if lazys.all? { |maybe| maybe.is_some? } + [self.get] + lazys.map(&:get) + else + nil + end + } end def self.combine(*maybes) - if maybes.any?(&:is_none?) - None() - else - values = maybes.map(&:get) - Maybe(values) - end + first, *rest = *maybes + first.combine(*rest) end private @@ -137,6 +139,14 @@ def self.===(other) super || (other.class == Maybe && other.is_some?) end + def combine(*maybes) + if maybes.all? { |maybe| maybe.is_some? } + Maybe([self.get] + maybes.map(&:get)) + else + None() + end + end + def method_missing(method_sym, *args, &block) map { |value| value.send(method_sym, *args, &block) } end @@ -190,6 +200,10 @@ def method_missing(*) self end + def combine(*) + self + end + private def __enumerable_value diff --git a/spec/spec.rb b/spec/spec.rb index faaa4d5..f8bd1bf 100644 --- a/spec/spec.rb +++ b/spec/spec.rb @@ -218,6 +218,22 @@ def test_case_when(case_value, match_value, non_match_value) expect(Maybe.combine(Maybe("foo"), Maybe(nil)).get_or_else { false }).to eql(false) expect(Maybe.combine(Maybe("foo"), Maybe(nil)) { |a, b| "#{a} + #{b}" }.get_or_else { false }).to eql(false) end + + it "is lazy" do + called1, called2, called3, combine_called, map_called = false, false, false, false, false + + m = Maybe { called1 = true ; 1 }.combine(Maybe { called2 = true ; 2 }, Maybe { called3 = true ; 3 }) + + sum = m.map { |(v1, v2, v3)| map_called = true ; v1 + v2 + v3 } + + expect(called1).to eql false + expect(called2).to eql false + expect(called3).to eql false + expect(combine_called).to eql false + expect(map_called).to eql false + + expect(sum.get).to eql 6 + end end describe "laziness" do From e58ae36834791bfe14f51b5a2af86eec1234716b Mon Sep 17 00:00:00 2001 From: Mikko Koski Date: Mon, 25 Aug 2014 09:27:45 +0300 Subject: [PATCH 12/12] Fix tests for 1.9.3 --- spec/spec.rb | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/spec/spec.rb b/spec/spec.rb index f8bd1bf..8ef1b81 100644 --- a/spec/spec.rb +++ b/spec/spec.rb @@ -220,19 +220,23 @@ def test_case_when(case_value, match_value, non_match_value) end it "is lazy" do - called1, called2, called3, combine_called, map_called = false, false, false, false, false + called1, called2, called3, map_called = false, false, false, false m = Maybe { called1 = true ; 1 }.combine(Maybe { called2 = true ; 2 }, Maybe { called3 = true ; 3 }) sum = m.map { |(v1, v2, v3)| map_called = true ; v1 + v2 + v3 } - expect(called1).to eql false - expect(called2).to eql false - expect(called3).to eql false - expect(combine_called).to eql false - expect(map_called).to eql false + expect(called1).to eql(lazy_enabled ? false : true) + expect(called2).to eql(lazy_enabled ? false : true) + expect(called3).to eql(lazy_enabled ? false : true) + expect(map_called).to eql(lazy_enabled ? false : true) expect(sum.get).to eql 6 + + expect(called1).to eql(true) + expect(called2).to eql(true) + expect(called3).to eql(true) + expect(map_called).to eql(true) end end