Rubyで関数型プログラミング#3: フロー制御(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

Rubyで関数型プログラミング#3: フロー制御(翻訳)

関数型プログラミングにおける「フロー制御」というアイデアは、主にオブジェクト指向言語や命令形言語を使ってきたプログラマーにとって少々腑に落ちにくいところがあります。意外かもしれませんが、関数型プログラミングにおいて「例外処理」は望ましくないものとみなされています。

だとすると、そのアイデアをどうやって現在のRubyでやっていることと折り合いをつければよいのでしょうか?特にこの点が問題にされやすいようです。おわかりのように、現在のRubyコードのかなりの部分に適用できるのです。早速見てみましょう!

ここでご注意いただきたいのですが、本記事ではコードをあえて「明示的に」書いています。より簡潔なコードではyieldreduceなどのツールを使うことでしょう。このあたりを掘り下げたい方は私の記事「Enumerableをreduceで徹底理解する#1 基本編」をご覧ください。

本当の意味での例外

ここで最初に強調しておきたいのは、例外とは何なのかについてです。短い答えは「私たちが本来期待していない例外的な振る舞い」となります。

次のfindメソッドの変形で考えてみましょう。

def find(array, &fn)
  array.each do |v|
    return v if fn.call(v)
  end

  raise "何も見つからない!"
end

何か見つかれば値を1つ得られますが、見つからない場合はどうなるでしょう。例外を1つ受け取り、クラッシュする前にキャッチしなければなりません。

find([1,2,3]) { |v| v == 4 } rescue nil

これを何度も書いているうちに、だんだん嫌気が差してきます。これはどう見ても「例外的な振る舞い」ではなく、「何も見つからない場合もある」と思いっきり想定しています。しかもコードにはrescue nilまであるではないですか!

例外時に結局nilを返すのなら、次のように普通にnilを返せばよいことです。

def find(array, &fn)
  array.each do |v|
    return v if fn.call(v)
  end

  nil
end

健全なデフォルト値

このことから、「健全なデフォルト値」という概念を得られます。データが入力から出力まで滞りなく流れるようにするために、扱う型を一貫させたいのです。ここで、仮にselectメソッドが、findで何もマッチしなかった場合のように振る舞うとしましょう(訳注: RubyのEnumerable#findは何もマッチしない場合にnilを返します)。

def select(array, &fn)
  found_items = []

  array.each do |v|
    found_items << v if fn.call(v)
  end

  return nil if found_items.empty?
  found_items
end

この例は込み入っていますが、仮にそのようなことを行い、かつそれがEnumerableのメソッドの基礎だとすると、これをチェインするのは至難の業です。

[1, 2, 3]
  .select { |v| v > 3 }
  .map { |v| v * 2 }

上はおそらくクラッシュするでしょう。selectが空の配列を返すなど、健全なデフォルト値を使えば、nilチェックを繰り返す必要もなければ例外の恐れもなく、自由自在にチェインできるようになります。

以上がRubyにおけるフロー制御です。型(配列)のようにつなげることで、エッジケースのためにいちいち立ち止まってチェックすることなく、入力から出力までデータが滞りなく流れるようになります。

しかし面白くなってくるのはここからです。戻される型が一貫し、そこからチェインしてもよい健全なデフォルト値が使えるなら、他にどんなやり方が考えられるでしょうか?ここからは、既に踏み固められたRubyの道筋からほんの少し離れて、Option(オプション)という概念を導入します。

訳注: Optionは関数型プログラミングに頻出する用語です — 関数型プログラミングのパターン - Qiita

あれもこれも実はOptionだった

さてOptionとは一体何者でしょうか?Optionは「something」か「nothing」のいずれかとして存在し、それに応じてデータの流れ方を制御できます。今は、データを入れる1つの箱だとお考えください。

class Option
  attr_reader :value

  def initialize(v)
    @value = v
  end
end

Option.new(5)
# => #<Option:0x00007fb2ae99d0c8 [@value](http://twitter.com/value "Twitter profile for @value")=5>

ここに入れられた値は、Optionが提供するAPIを使わないと変更できません。今はろくなAPIがないので、箱に値を入れただけでは嬉しくも何ともありません。

先に進むために、この値を変換する手段が欲しいということにします。それではmapを追加してみましょう。

class Option
  attr_reader :value

  def initialize(v)
    @value = v
  end

  def map(&fn)
    Option.new(fn.call(@value))
  end
end

Option.new(5).map { |v| v * 2 }
# => #<Option:0x00007fb2b02748f8 [@value](http://twitter.com/value "Twitter profile for @value")=10>

関数型プログラミングらしくするため、値を変換するたびに新しいOptionを返します。ここにmapを好きなだけいくつでもチェインできるようになりましたが、次のような場合はどうでしょうか?

Option.new(5).map { |v| nil }.map { |v| v * 2 }
# NoMethodError: undefined method `*' for nil:NilClass

あれま!またしても例外です。ここで必要なのは、mapできるものが実際にあることをOptionが知る方法か、それがない場合に何もしないようにする方法です。

今度は、リターンが正しいことを示す「nothing」が正式に欲しいとしましょう。値が怪しい場合にまとめてブラックホールに放り込むわけにはいきません。そんなことをしたらフロー制御が台無しになります。しかしそれを切り出す方法があるとしたらどうでしょうか。

前回の記事の「現実におけるクロージャ」セクションをご覧ください。

ガードブロック(Guard Block)マッチャーは、「something」か「nothing」かという概念を扱い、さらにそこにステートをわずかに追加することで、怪しい戻り値を合理的に扱う手段も提供します。ここではArrayを用いており、私はこれを「箱」にかなり近いものだと思っています。

[true, VALUE][false, false]を与えたとすると、これらの実際の名前は何になるでしょうか。ScalaやRustでの呼び名と同様、それぞれ「Some」と「None」という呼び名になります。

『タダで手に入るもの』

訳注: 見出しはSomething for Nothingにかけたダジャレです。

では、ここでSomeNoneはどんな考え方になるでしょうか?今、Someにはmapしたいと思っている値が1つあります。そしておそらくNoneについては単に無視したいと思っています。

class Option
  attr_reader :value

  def initialize(v = nil)
    @value = v
  end

  class << self
    def some(v) Some.new(v) end
    def none()  None.new()  end
  end
end

class Some < Option
  def map(&fn)
    new_value = fn.call(@value)

    new_value.nil? ?
      Option.none() :
      Option.some(new_value)
  end

  def otherwise(&fn)
    @value
  end
end

class None < Option
  def map(&fn)
    self
  end

  def otherwise(&fn)
    fn.call
  end
end

SomeはちょうどOptionと同じように、それに与える値は何でもmapします。しかしNoneの方はもう少し興味深いものです。Nonemapしようとする試みをすべて無視しますが、チェインの末端でotherwise関数を呼んでくれるのです。

Option.some(5)
  .map { |v| v * 2 }
  .map { |v| v * 5 }
  .otherwise { 0 }
# => 50

Option.some(5)
  .map { |v| v * 2 }
  .map { |v| nil }
  .otherwise { 0 }
# => 0

これはさまざまな名前で呼ばれている手法ですが、私好みの呼び名は、以下の良記事で唱えられている「線路指向プログラミング(Railway Oriented Programming)」です。

参考: Railway Oriented Programming | F# for fun and profit ↓以下は同記事のスライドです。

良くない値を受け取ったその瞬間、線路のポインタを切り替えてデータという名の列車を安全に走らせ続け、線路の末端に辿り着いたらotherwisevalueのいずれかの値を明示的に取り出すというものです。

今のはOptionだったの?

もちろんrescue文をある程度避けられるコードは他にもいろいろ考えられそうですが、ここで受け取っているのは、より受け取る値打ちのあるものです。すなわち、アプリを一貫して流れる明示的なステートです。

今や私たちは、関数のあらゆるケースをカバーすることと、関数がSomethingまたはNothingを返すかどうかを定義することを非常にはっきりと強制されます。それによって、パイプライン内のどこでエラーが刈り込まれているかを極めてはっきりと確認できるようになります。

このような明示的な強制を期待するScalaやRustやHaskellといった言語は、皮肉にも(訳注: 強制という言葉と裏腹に)私たちを解放してくれるものなのです。たったひとつのレアケースをカバーできているかどうかという心配の種から私たちを解放し、私たちが無意識に当然のことだと仮定しているおびただしい懸念事項を浮かび上がらせてくれます。

とはいうものの、今回も純粋なRubyの道から少々それています。つまりもうあなたは、さまざまな外部ライブラリをこんなふうにラップしてサンドボックスで楽しく遊べるようになっていることでしょう。

既存の実例はどこかにあるの?

今回ご紹介したアイデアで多少遊んでみたいのであれば、既に以下のようなさまざまな実装が世に出現しています。その多くはOptionという名前ではなくMaybeSomeではなくJustNoneではなくNothingと呼ばれる傾向がありますので、こちらの方をこれまでに目にしたことのある方もいるかもしれません。

お気づきのように、このイカれた語はすなわちモナド(monad)です。本記事に登場しているのは厳密な意味でのモナドではありませんし、今はモナドという言葉をさほど気にする必要もありません。

もっと馴染みのある言葉になぞらえれば、これはBuilderパターンに相当するかもしれません。nilを受け取ったときに振る舞いが変わるBuilderです。ActiveRecordのクエリやPromise、Enumerableなどは、こうした概念のある種の近似として興味が尽きません。

モナドの詳細はまぶしいほどの輝きを放っていますが、たとえ(数学の)証明を持ち出すほど深掘りしなくても、こうした概念を学ぶことでその価値がさらに多くの輝きを増す余地があります。

締めくくり

今回はフロー制御をかなり駆け足でご紹介したこともあり、消化すべき内容も盛りだくさんでした。こうした概念をもっともっと学んでみたいのであれば、他の言語向けの興味深い記事やガイドが多数あります。

(特に私にとって)学ぶことは山ほどありますので、皆さんも学ぶばかりでなくたまには自分で記事にまとめてみましょう。あなたの書く記事が思わぬ人にとって役に立つかもしれません。

関数型プログラミングを楽しみましょう。

関連記事

Rubyで関数型プログラミング#1: ステート(翻訳)

Rubyで関数型プログラミング#2: クロージャ(翻訳)

Ruby: Enumerableを`reduce`で徹底理解する#1 基本編(翻訳)

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の半分ほど、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れてそれぞれ一部を翻訳。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好き。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

週刊Railsウォッチ

インフラ

ActiveSupport探訪シリーズ