関数合成のススメ 〜 オブジェクト指向プログラマへ捧げる関数型言語への導入その1
こんにちわ。今日は、関数合成の話をします。
標準入力から何行か読み込んで、ソートして返す以下のようなScalaのコードを例にします。
例なので、あえて冗長なコードを書いてます。
このコードでは、4つの関数が用意されています(unlines/putstr/sort/lines)。
この4つの関数を組み合わせて、読み込んだ文字列を行に分解して、ソートして、
また改行コードをつけて文字列にもどして表示、ということをやっています。
object Main extends App { val in = scala.io.Source.stdin.getLines.mkString("\n") // 改行をつけて結合する関数 val unlines = (s:Seq[String]) => s.mkString("\n") // 文字列を表示する関数 val putStr = (s:String) => println(s) // ソートする関数 val sort = (s:Seq[String]) => s.sorted // 文字列を改行コードで分割する関数 val lines = (s:String) => s.lines.toSeq // かっこがいっぱい……。 putStr( unlines( sort( lines(in)))) }
問題は、最後の行です。
// かっこがいっぱい……。
putStr( unlines( sort( lines(in))))
このカッコを無くすことが、今回のテーマです。
合成しなイカ
数学の授業で習った、合成関数を思い出してみましょう。
(g ∘ f)(x) = g(f(x))
関数gと関数fの合成関数g ∘ fに引数xを渡した結果は、関数gにf(x)の結果を渡したものと等しい、と言うことです。
先ほどのコードの例に戻りましょう。関数gをputStr, fをunlines(...)に置き換えると
(putStr ∘ unlines)(sort(lines(in)) = putStr( unlines( sort( lines(in))))
となります。この調子で、sortやlinesも合成していくと、このように変形できます。
(putStr ∘ unlines ∘ sort ∘ line)(in)
さて、Scalaにおける関数の合成は、Function1トレイトにcomposeというメソッドとして定義されています。これを利用すると、
(putStr compose unlines compose sort compose lines)(in)
このようになりました。カッコが減っていますね。
読みにくくなイカ?!
composeを使って関数を合成できることが分かりましたが、そもそもcomposeって長いのでもうヤ。
そこで、Groovyのように<<で関数を合成できるようにします。Function1に対して、このようなimplicit conversionを
定義します。
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 } implicit def toComposableFunction1[T1, R](func:T1 => R) = new ComposableFunction1[T1, R]{ val f = func }
その結果、先ほどのcomposeは<<で置換できるので、
(putStr << unlines << sort << lines)(in)
おー、だいぶシンプルになったじゃなイカ!
適用しなイカ?!
まだちょっと不満な点があります。それは、(合成関数)(引数)のように、まだカッコが残っているからです。
「全てのカッコを、書かれる前に消し去りたい。全てのコード、過去と未来の全てのカッコを、この手で…!」
そこで、composeに<<という別名を付けたように、関数適用applyにも別名を付けます。ここでは、<*>をapplyの別名として、
先ほど用意したimplicit conversionで変換されるComposableFunction1に追加します。
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 // applyの別名 def <*>(v:T1) = f apply(v) }
さぁ、さっきのコードを変形してみようじゃなイカ?
putStr << unlines << sort << lines <*> in
やったー、カッコが消えたよ!!。
さて、これをもう少し推し進めると、Applicativeスタイルになるんじゃなイカ?と思ったのですが、今回はここまで。
ちなみにHaskellでは
import List main = do cs <- getContents sorting cs sorting :: String -> IO () sorting cs = putStr (unlines (sort (lines cs)))
が
sorting :: String -> IO () sorting cs = putStr $ unlines $ sort $ lines cs
こうなって
sorting :: String -> IO () sorting = putStr . unlines . sort . lines
こうなります。