関数適用のススメ 〜 オブジェクト指向プログラマへ捧げる関数型言語への導入その2
こんにちわ。今日は、関数適用の話をします。
前回のエントリで、「メソッドチェーンと違いがよくわからない」という指摘をもらったり、なんかスゴイSmalltakerから黒魔術を駆使したトラックバックをもらったりしたので、もう少し前回のエントリの意図するところを掘り下げたい、と思います。
ちなみに、このシリーズは、自分が学習して考えた結果をまとめたもので、タイトルにある「オブジェクト指向プログラマ」というのは俺自信のことです(キリッ
ごめんなさい。
前回まで
前回は、このような入れ子になった関数呼び出しを、
// かっこがいっぱい……。
putStr(unlines(sort(lines(in))))
関数合成を駆使して、このように変換するところまで説明しました。
putStr << unlines << sort << lines <*> in
オブジェクトが無い世界
本題に入る前に、「オブジェクト指向プログラマのこと何ぞいっさい顧みてねーじゃねーか」という指摘もあったので、今回のお話の前提をまず設定します。
今説明している世界は、「オブジェクトが無い世界」です。あるのは、関数だけです。
前回の、「標準入力から何行か読み込んで、ソートして返す」処理ですが、実はScalaでもこのようなメソッドチェーンを利用して書くことが可能です。というか、通常はこう書きます。
println( in.lines.toSeq.sorted.mkString("\n") )
これは、StringオブジェクトやSeqオブジェクトが持つメソッドを利用していますが、今回は「オブジェクトが無い世界」のお話なので、この方法はいったん忘れてください。そもそもの、パラダイムが違うのでゲソ。
記事の後半で、クラス作ったりimplicit conversionしたりしてますが、あくまでコードの意図を実現するための小細工ですので、そこはご容赦を。
しかし、関数型言語はオブジェクト指向なプログラミング言語より非力ではないか、というと、実はそうでもなくて、代わりに関数が第一級の値(ファーストクラス)であり、合成して新しい関数をアドホックに作成することで、柔軟な表現力を獲得している、と思っています。
今回の例では、「関数」というオブジェクトだけが存在して(オブジェクトあるじゃねーか、というツッコミはなしでゲソ)、その「関数」が「合成する」というメソッドや、「値を適用する」というメソッドを持っている、という想定で話をします。
では、本題に入ります。
関数適用じゃないイカ!
前回は、関数適用(ようは、呼び出し)に、<*>という別名をつけました。最初の、かっこが多いバージョンのコードに、この<*>を明示的に書いてみると、イカのような形になります。
// putStr(unlines(sort(lines(in))))
putStr <*> (unlines <*> (sort <*> (lines <*> in)))
全部で、4箇所に<*>が登場しているので、4回の関数適用が行われている事がわかります。逆に、カッコを無くしたバージョンでは、
putStr << unlines << sort << lines <*> in
<*>なので、関数適用は1回です。つまり、putStr/unlines/sort/linesを合成した「大きな関数」に一度だけ関数適用を行っている、と言う形です。
もっと合成しなイカ!
では、putStr/unlines/sort/linesを合成した「大きな関数」を、sortingという変数に束縛(代入でもいいです)しておきましょう。関数はファーストクラスなので、適当な変数に束縛したり、別な関数に渡すことが可能だからです。
// 引数に文字列を受け取って標準出力にソート結果を表示する関数オブジェクト val sorting = putStr << unlines << sort << lines
このsorting関数は、引数に文字列を受け取って標準出力にソート結果を表示します。
次に、ファイル名を引数にとって読み込み、文字列を返すreadFile関数を定義しましょう。
val readFile = (filename:String) => scala.io.Source.fromFile(new java.io.File(filename)).getLines.mkString("\n")
先ほどのsorting関数と合成することで、ファイルから読み込んだ内容をソートする関数を新しく作り出すことができます。
val fileSorting = sorting << readFile fileSorting <*> "test.txt"
オブジェクト指向では、継承や委譲を利用してメソッドを組み合わせることでプログラミングを行いますが、関数型の世界では、この例のように、ある関数に新しい関数を合成したり、関数に関数を渡したりしてプログラミングしていく、という感覚です。
左から読まなイカ?!
通常のプログラマは、このようにカッコがネストしているコードを詠むときに、無意識にカッコの内側から処理が行われる、と読んでいるはずです。
putStr(unlines(sort(lines(in))))
上記のコードだと、「linesにinを渡して、その結果をsortにながして、さらにunlinesに……。」といった具合です。いわば、右から左へコードを詠んでいる、とも言えます。
ですが、我々は文章を左から右に読むことに慣れ親しんでいるので、プログラムだけ右から左に読まなければいけない、というのはちょっとどうかと思うわけです。みなさんは、訓練のたまものによって、違和感なくコードを読めているだけだと言えます。
そこで、関数の合成順を逆にできれば、左から右に処理を行わせるような書き方を実現できます。前回、Fuction1型へのimplicit conversionで、関数を合成する<<メソッドを追加しました。このメソッドは、「引数の関数gの結果を自分(自分自身も関数オブジェクト)に適用する関数」を合成する、ということを行います。この順番を、逆にしたらどうでしょうか?
つまり、「自分自身に値を適用した結果を、引数の関数gに渡す関数」へ合成できるようなメソッドを用意するのです。実は、もう用意してあって、>>メソッドがコレにあたります。
trait ComposableFunction1[-T1, +R] { val f: T1 => R def >>[A](g:R => A):T1 => A = f andThen g def <<[A](g:A => T1):A => R = f compose g def <*>(v:T1) = f apply(v) }
この<<と>>の違いは、イカの例を見てもらえれば分かるかと思います。
scala> val addFoo = (s:String) => s + "Foo " addFoo: String => java.lang.String = <function1> scala> val addBar = (s:String) => s + "Bar " addBar: String => java.lang.String = <function1> scala> (addFoo << addBar) <*> "おっぱい" res19: java.lang.String = "おっぱいBar Foo " scala> (addFoo >> addBar) <*> "おっぱい" res20: java.lang.String = "おっぱいFoo Bar "
では、この>>を使って、左から読めるコードにしてみましょう。こうなります。
(readFile >> lines >> sort >> unlines >> putStr) <*> "test.txt"
「readFileして、linesして、sortして……」というように、左から読んだ順と処理の順番が一致しています。実は、このようなコードの方が、自然に近いのではないでしょうか?
もっと左から読まなイカ?!
さきほど、>>を利用してコードを左から読めるように改修しましたが、ひとつ問題点があります。
(readFile >> lines >> sort >> unlines >> putStr) <*> "test.txt"
readFileに渡す引数"test.txt"が、依然として一番右に置かれていて、readFileと離れた位置にある点です。文章として読むなら、「"test.txt"を、readFileに渡して、lineを……。」というように、引数が一番左に来るのがいいでしょう。
では、どうすればいいのかというと、"test.txt"に、「関数を引数にとって、その関数に自信を値として渡して評価する機能」があればいいわけです。このような機能をもつコンテナを、ここでは仮にApと呼んでおきます。
class Ap[A](v:A){ def apply[B](f: A => B) = f(v) }
このApクラスは、コンストラクタに受け取った値を保持しておいて、applyメソッドで「関数を引数に取って自信を適用する」機能を持ちます。
なお、このApクラスは、一般的に言うApplicativeとは全く違うものなので、誤解無きようお願いします。
では、Apクラスを使ってみましょう。
(new Ap("test.txt")) apply readFile >> lines >> sort >> unlines >> putStr
引数"test.txt"が一番左に来たので、より自然に近い順番になりました。ここで、applyメソッドの別名を、前回と同じく<*>とし、StringからApクラスへのimplict conversionを定義してやると、
class Ap[A](v:A){ def apply[B](f: A => B) = f(v) def <*>[B](f:A => B) = f(v) } implicit def toAp(v:String) = new Ap(v)
このように書けてしまいます。
"test.txt" <*> (readFile >> lines >> sort >> unlines >> putStr)
今回は、この<*>という「関数適用を行う演算子」をApクラスとimplicit convesionによって実現していますが、言語仕様としてこのような演算子を持つプログラミング言語が存在してもおかしくはありません。一般的なプログラミング言語では、()によって関数適用が表現されているだけ、とも言えます。
ようはなにが言いたいかというと
- 小さな関数を合成して大きな関数を作るのが、関数型のやり方
- 合成と適用を分けて考えると、合成した関数自体を保存しておいて、別な関数と合成したり、引数に渡したりできる
- その結果、カッコがネストしたコードを、左から読めるようなコードに書き換えることができる
次回は、再帰とラムダ計算の話をしようと思うゲソ。