概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: Functional Programming in Ruby — Flow Control – Brandon Weaver – Medium
- 原文公開日: 2018/05/24
- 著者: Brandon Weaver
Rubyで関数型プログラミング#3: フロー制御(翻訳)
関数型プログラミングにおける「フロー制御」というアイデアは、主にオブジェクト指向言語や命令形言語を使ってきたプログラマーにとって少々腑に落ちにくいところがあります。意外かもしれませんが、関数型プログラミングにおいて「例外処理」は望ましくないものとみなされています。
だとすると、そのアイデアをどうやって現在のRubyでやっていることと折り合いをつければよいのでしょうか?特にこの点が問題にされやすいようです。おわかりのように、現在のRubyコードのかなりの部分に適用できるのです。早速見てみましょう!
ここでご注意いただきたいのですが、本記事ではコードをあえて「明示的に」書いています。より簡潔なコードではyield
やreduce
などのツールを使うことでしょう。このあたりを掘り下げたい方は私の記事「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にかけたダジャレです。
では、ここでSome
やNone
はどんな考え方になるでしょうか?今、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
の方はもう少し興味深いものです。None
はmap
しようとする試みをすべて無視しますが、チェインの末端で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 ↓以下は同記事のスライドです。
良くない値を受け取ったその瞬間、線路のポインタを切り替えてデータという名の列車を安全に走らせ続け、線路の末端に辿り着いたらotherwise
かvalue
のいずれかの値を明示的に取り出すというものです。
今のはOptionだったの?
もちろんrescue
文をある程度避けられるコードは他にもいろいろ考えられそうですが、ここで受け取っているのは、より受け取る値打ちのあるものです。すなわち、アプリを一貫して流れる明示的なステートです。
今や私たちは、関数のあらゆるケースをカバーすることと、関数がSomethingまたはNothingを返すかどうかを定義することを非常にはっきりと強制されます。それによって、パイプライン内のどこでエラーが刈り込まれているかを極めてはっきりと確認できるようになります。
このような明示的な強制を期待するScalaやRustやHaskellといった言語は、皮肉にも(訳注: 強制という言葉と裏腹に)私たちを解放してくれるものなのです。たったひとつのレアケースをカバーできているかどうかという心配の種から私たちを解放し、私たちが無意識に当然のことだと仮定しているおびただしい懸念事項を浮かび上がらせてくれます。
とはいうものの、今回も純粋なRubyの道から少々それています。つまりもうあなたは、さまざまな外部ライブラリをこんなふうにラップしてサンドボックスで楽しく遊べるようになっていることでしょう。
既存の実例はどこかにあるの?
今回ご紹介したアイデアで多少遊んでみたいのであれば、既に以下のようなさまざまな実装が世に出現しています。その多くはOption
という名前ではなくMaybe
、Some
ではなくJust
、None
ではなくNothing
と呼ばれる傾向がありますので、こちらの方をこれまでに目にしたことのある方もいるかもしれません。
- dry-rb - dry-monads - Introduction
- pzol/monadic: helps dealing with exceptional situations, it comes from the sphere of functional programming and bringing the goodies I have come to love in Scala to my ruby projects
- lazebny/ramda-ruby: Ruby port of http://ramdajs.com
お気づきのように、このイカれた語はすなわちモナド(monad)です。本記事に登場しているのは厳密な意味でのモナドではありませんし、今はモナドという言葉をさほど気にする必要もありません。
もっと馴染みのある言葉になぞらえれば、これはBuilderパターンに相当するかもしれません。nil
を受け取ったときに振る舞いが変わるBuilderです。ActiveRecordのクエリやPromise、Enumerableなどは、こうした概念のある種の近似として興味が尽きません。
モナドの詳細はまぶしいほどの輝きを放っていますが、たとえ(数学の)証明を持ち出すほど深掘りしなくても、こうした概念を学ぶことでその価値がさらに多くの輝きを増す余地があります。
締めくくり
今回はフロー制御をかなり駆け足でご紹介したこともあり、消化すべき内容も盛りだくさんでした。こうした概念をもっともっと学んでみたいのであれば、他の言語向けの興味深い記事やガイドが多数あります。
(特に私にとって)学ぶことは山ほどありますので、皆さんも学ぶばかりでなくたまには自分で記事にまとめてみましょう。あなたの書く記事が思わぬ人にとって役に立つかもしれません。
関数型プログラミングを楽しみましょう。