前置き
関数型というタイトルは釣りです。
先日、こんな命題を頂いた
命題: 以下の様な処理はどう書くべきか?
def foo(value)
value = value.filter {|x| x % 2 == 0 }
if value.empty?
p "偶数の値がないよ!"
return
end
value = value.filter {|x| x > 5 }
if value.empty?
p "5より大きい値がないよ"
return
end
end
foo [1,2,3,4,5,6]
この命題は「関数型っぽく」というか、メソッドチェーンを断ち切らずに、
そして、例外等の大域脱出は使わずに処理は書けるかという問題だと理解した。
(この命題、実際にはJavaというかGroovyで出されたものであるが、慣れていないのでRubyで記述する)
そして、私の想像した答えはこんな感じだった。
def foo(value, f, g)
value.filter {|x| f.call(x) }
.if_empty {
p "error f()"
}
.filter {|x| g.call(x) }
.if_empty {
p "error g()"
}
end
さて?これは実装できるのか?
Optional を使う
私は当初、失敗を表現するならOptionalだろうと考えていた。OptionalというのはJava8での名前で、HaskellならMaybe、ScalaならOptionのことである。
上記で、if_empty というのもJavaでorElse()というメソッドがあり、これが上記の目的で使えると思っていた。しかし、実際に試したところorElse()はOptionalを返さないのでメソッドチェーンできない。そこで、if_empty というメソッドをでっちあげてこの問題に対処してみた。
具体的には、以下の実装を考えてみた・・・がそれでもうまくいかない。
(ここでは、問題を簡単にするためvalueがコレクションであることは一旦考えない)
class Optional
attr_reader :value
def initialize(x)
@value = x
end
def self.of(x)
Optional.new(x)
end
def empty?
@value.nil? || @value.empty?
end
def each
unless self.empty?
yield
end
self
end
def if_empty
if self.empty?
yield
Optional.of(nil)
else
self
end
end
def filter
if yield @value
self
else
Optional.of(nil)
end
end
def map
if self.empty?
Optional.of(nil)
else
Optional.of(yield @value)
end
end
def flat_map
if self.empty?
Optional.of(nil)
else
yield @value
end
end
EMPTY = Optional.of(nil)
def inspect
if empty?
"Optional::EMPTY"
else
"Optional.of(#{@value.inspect})"
end
end
def ==(other)
@value == other.value
end
end
def foo(value, f, g)
value.filter {|x| f.call(x) }
.if_empty {
p "error f()"
}
.filter {|x| g.call(x) }
.if_empty {
p "error g()"
}
end
def assert(expect, actual)
if expect != actual
puts <<-END
at #{caller(1)[0]}
expect: #{expect.inspect}
actual: #{actual.inspect}
END
end
end
p 1; assert Optional.of("a"), foo(Optional.of("a"), proc{true }, proc{true })
p 2; assert Optional::EMPTY, foo(Optional.of(nil), proc{false}, proc{false})
p 3; assert Optional::EMPTY, foo(Optional.of("a"), proc{false}, proc{true })
p 4; assert Optional::EMPTY, foo(Optional.of("a"), proc{true }, proc{false})
p 5; assert Optional::EMPTY, foo(Optional.of("a"), proc{false}, proc{false})
ここで、実装したOptionalについて簡単に説明すると、
* Optional.of(x)
で、x を Optional コンテナに入れる
* Optional#filter {...}
は、ブロックが真を返したらselfを返し、偽を返したらEMPTY(失敗)を返す
* Optional#if_empty {...}
は、selfがEMPTYならブロックを実行してEMPTYを返す。selfがEMPTYでなければselfを返す
これによって、当初想像した解を表現できるかと思ったが以下の問題があった。
- if_empty がEMPTYを返したら残りのブロックは空振りして欲しい。しかし後続のif_emptyがもう一度実行されてしまう
value.filter {|x| f.call(x) }
.if_empty {
p "error f()" <-- 前のfilterの結果がEMPTYならこれが実行され、EMPTYを返す
}
.filter {|x| g.call(x) } <-- EMPTYなので、これは実行されない。OK
.if_empty {
p "error g()" <-- EMPTYが伝搬しているのでこれも実行されてしまう!
}
どうも違うらしい。そこでいろいろ考え直したところ以下のようになった。
しかしこれは、最初の命題からreturnがなくなっただけである。何もおいしくない。
def foo(value, f, g)
value.map {|x|
if f.call(x)
x
else
p "error f()"
nil
end
}
.map {|x|
if g.call(x)
x
else
p "error g()"
nil
end
}
end
Either を使う
Optional だと、値またはEMPTYしか返せないので、Eitherにより値または失敗オブジェクトを返すようなものを考えてみる。
ただし、Haskellの入門に何度か挫折しているので、HaskellのEitherがどういったものかは理解できていない。ここでは、データとエラーオブジェクトを保持し、今回の目的を達成するメソッドを持ったクラスとしてOptional2を定義した。
そして、if_empty は、空で勝つエラーがない場合にブロックを実行しエラーをセットするメソッドとした
class Error < RuntimeError
def initialize(message = nil)
@message = message
super(message)
end
attr_accessor :message
end
class Optional2
attr_reader :error, :value
def initialize(error, value)
@error = error
@value = value
end
def self.of(value)
Optional2.new(nil, value)
end
def self.empty(message = nil)
Optional2.new(Error.new(message), nil)
end
def empty?
@value.nil? || @value.empty?
end
def each
unless self.empty?
yield
end
self
end
def if_empty
if self.empty? && (self.error.nil? || self.error.message.nil?)
Optional2.empty(yield)
else
self
end
end
def filter
if self.empty?
self
elsif yield @value
self
else
Optional2.empty
end
end
def map
if self.empty?
self
else
Optional2.of(yield @value)
end
end
def flat_map
if self.empty?
self
else
yield @value
end
end
def inspect
if empty?
"Optional2::EMPTY(#{@error.inspect})"
else
"Optional2.of(#{@value.inspect})"
end
end
def ==(other)
@value == other.value &&
@error == other.error
end
end
def foo(value, f, g)
value.filter {|x| f.call(x) }
.if_empty {
"error f()"
}
.filter {|x| g.call(x) }
.if_empty {
"error g()"
}
end
def assert(expect, actual)
if expect != actual
puts <<-END
at #{caller(1)[0]}
expect: #{expect.inspect}
actual: #{actual.inspect}
END
end
end
p 1; assert Optional2.of("a"), foo(Optional2.of("a"), proc{true }, proc{true })
p 2; assert Optional2.empty("error f()"), foo(Optional2.of(nil), proc{false}, proc{false})
p 3; assert Optional2.empty("error f()"), foo(Optional2.of("a"), proc{false}, proc{true })
p 4; assert Optional2.empty("error g()"), foo(Optional2.of("a"), proc{true }, proc{false})
p 5; assert Optional2.empty("error f()"), foo(Optional2.of("a"), proc{false}, proc{false})
これで目的は達成した。しかし、これは一般的な解放なのだろうか?
一応、最初の命題のもこれで達成する
def foo(value)
value.map {|x| x.select {|v| v % 2 == 0 }}
.if_empty {
"偶数の値がないよ!"
}
.map {|x| x.select {|v| v > 5 }}
.if_empty {
"5より大きい値がないよ"
}
end
p foo Optional2.of([1,2,3,4,5,6])
この、Optional2の世界から値またはエラーを取り出すインタフェースをどうするべきかまでは考えてない。