読者です 読者をやめる 読者になる 読者になる

( ꒪⌓꒪) ゆるよろ日記

( ゚∀゚)o彡°オパーイ!オパーイ! ( ;゚皿゚)ノシΣ フィンギィィーーッ!!!

関数合成のススメ 〜 オブジェクト指向プログラマへ捧げる関数型言語への導入その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


こうなります。