LoginSignup
2
1

More than 5 years have passed since last update.

関数型的に失敗を表すには

Last updated at Posted at 2016-05-15

前置き

関数型というタイトルは釣りです。

先日、こんな命題を頂いた

命題: 以下の様な処理はどう書くべきか?

  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の世界から値またはエラーを取り出すインタフェースをどうするべきかまでは考えてない。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1