Skip to content

Commit

Permalink
Merge pull request #4 from rap1ds/threequals
Browse files Browse the repository at this point in the history
Implement threequals so that Maybe can be used in case expressions
  • Loading branch information
rap1ds committed Jul 23, 2014
2 parents b66c24b + 20fab23 commit 149c847
Show file tree
Hide file tree
Showing 3 changed files with 193 additions and 93 deletions.
60 changes: 55 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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::Some:0x007ff7a85621e0 @value="I'm a value">
Maybe(nil) => #<Maybe::None:0x007ff7a852bd20>
Maybe("I'm a value") => #<Some:0x007ff7a85621e0 @value="I'm a value">
Maybe(nil) => #<None:0x007ff7a852bd20>
```

Both `Some` and `None` implement four trivial methods: `is_some?`, `is_none?`, `get` and `or_else`
Expand All @@ -51,19 +51,69 @@ 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::Some:0x007ff7ac8697b8 @value=2.0>
Maybe(nil).map { |v| Math.sqrt(v) } => #<Maybe::None:0x007ff7ac809b10>
Maybe(4).map { |v| Math.sqrt(v) } => #<Some:0x007ff7ac8697b8 @value=2.0>
Maybe(nil).map { |v| Math.sqrt(v) } => #<None:0x007ff7ac809b10>
Maybe(2).inject(3) { |a, b| a + b } => 5
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::Some:0x007ffe198e6128 @value="I'M A VALUE">
Maybe("I'm a value").upcase => #<Some:0x007ffe198e6128 @value="I'M A VALUE">
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`
Expand Down
148 changes: 73 additions & 75 deletions lib/possibly.rb
Original file line number Diff line number Diff line change
@@ -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
78 changes: 65 additions & 13 deletions spec/spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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

Expand All @@ -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)
Expand All @@ -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
Expand Down

0 comments on commit 149c847

Please sign in to comment.