「関数型Ruby」という病(6) - 関数合成と文脈、Proc#liftとProc#>=、そしてモナ
前回から一年以上が経過しているけど、最近lambda_driver.gemに機能を追加したので、そのことについて書こうと思う。
Rubyで、モナ……っぽい関数合成を実装した話だ。
Rubyで関数合成とかしたいので lambda_driver.gem というのを作った - ( ꒪⌓꒪) ゆるよろ日記
関数合成
関数合成については以前に書いたので、こちらを見て欲しい。
「関数型Ruby」という病(2) - 関数合成 Proc#compose - ( ꒪⌓꒪) ゆるよろ日記
おさらいをしておくと、関数合成とは、 関数gと関数fから、g(f(x))という関数hを新たに作り出すことだ。
(g ∘ f)(x) = g(f(x))
関数gと関数fの合成関数g ∘ fに引数xを渡した結果は、関数gにf(x)の結果を渡したものと等しい。つまり、このような操作である。
f = lambda{|x| x + 1 } g = lambda{|x| x * x } # 合成関数g ∘ f h = lambda{|x| g.(f.(x)) }
これを図にするとこんな感じ。 上記の例の合成関数h: g ∘ fに引数 3を与えた場合。
lambda_driver.gemでは、この関数合成をProc#>>で行うことができるようになっている。
# Proc#>>で合成 h = f >> g h.(3) # => 16
Proc#>>の実装は単純で、以下のようになる。
class Prco def >>(g) # 「自分の計算結果を引数の関数gへ渡す」Procオブジェクトを返す lambda{|x| g.call(self.call(x)) } end end
関数合成と計算の失敗
このように、とても便利な関数合成だが、以下のような状況だと少し困ることがある。
f = lambda{|arr| arr.first } # f: 引数のArrayの最初の要素を取り出す g = lambda{|x| x + 1 } # g: 引数に1を加算 h = lambda{|x| x * 2 } # h: 引数を2倍 # f, g, hを合成 i = f >> g >> i i.([3,5]) # => 4
関数fは、引数に配列を取って最初の要素を返す。関数gはfの結果に1を加算する。関数hはさらにその結果を2倍する。単純だ。
この3つの関数を合成した物が関数i。図にするとこうなる
では、関数iに空配列[]を渡すとどうなるか?
# []を渡すと i.([]) # => NoMethodError: undefined method `+' for nil:NilClass
関数fはnilを返し、関数gはnilに1を加算しようと+を呼び出してエラーになっている。
図ではこうなる。
ここで、関数がnilを返した場合はその計算は失敗したと仮定する。よって、関数gと関数hでは、引数がnilであるかチェックするように変更する。
# g, hに引数がnilかチェックを追加した g = lambda{|x| return nil if x.nil?; x + 1 } h = lambda{|x| return nil if x.nil?; x * 2 } i = f >> g >> h i.([]) # => nil
例外も発生せず、めでたしめでたし……ではない。関数gとiにそれぞれnilをチェックする処理が重複して実装されているのでDRYではない。合成する関数がもっと多くなった場合は面倒だ。
できればこのnilをチェックする処理を共通化したい。
関数合成に細工する
関数を合成するときに、「nilかどうか判定する処理」を間に挟むようにすれば、個々の関数にわざわざnilチェックを実装せずともよい。
以下のように関数合成時に細工を行うProc#>=を実装する。
class Proc def >=(g) lambda{|x| res = self.call(x) # 計算結果がnilならば、後続の関数gを呼び出さずにnilを返す return nil if res.nil? g.call(res) } end end
これで、Proc#>=を使って細工された関数合成を行うことで、計算が途中で失敗した場合は以降の計算を打ち切るようにできる。
f = lambda{|arr| arr.first } g = lambda{|x| x + 1 } h = lambda{|x| x * 2 } # Proc#>=で合成する i = f >= g >= h i.([3,5]) # => 8 i.([]) # => nil
これは、図にするとこのようなイメージである。
合成する関数がどれだけ増えようと問題がない。
j = lambda{|x| x * 3 } # 新たに関数jを合成 k = f >= g >= h >= j k.([3,5]) # => 24 k.([]) # => nil
こんどこそめでたしめでたし。
文脈付き関数合成
Proc#>=によって、関数合成の際に細工をすることで、「途中で計算が失敗したら打ち切る関数合成」を実現できた。
では、nilチェックのような「細工」を任意に指定できるようにしてはどうだろうか?
たとえば、「計算の途中結果をputsで標準出力に吐く」関数合成をしたいとする。
そのために、どのような「細工」をするかを設定するProc#liftメソッドを用意しよう。
class Proc def lift(ctx) # 引数の「細工」を行うProcオブジェクトをインスタンス変数に設定しておく @ctx = ctx # 自身の>=メソッドを定義する(特異メソッド) def self.>=(g) lambda{|x| # ctxに、合成する関数gと、自身の計算結果を渡して処理を「細工」する @ctx.call(g, self.call(x)) }.lift(@ctx) # liftの戻り値のProcオブジェクトも同様にliftしておく end self end end
少々トリッキーな実装なので解説すると、Proc#liftメソッドは細工を行うProcオブジェクト(ctx)を受け取る。
このProcオブジェクト(ctx)は、第一引数にProc#>=メソッドで渡された合成先の関数g、第二引数に合成元の関数fの計算結果であるxを受け取るようにしておく。
liftメソッド内では、特異メソッドとしてProc#>=メソッドを定義する。Proc#>=はインスタンス変数として記憶してあるctxに、合成する関数gと、自身の計算結果を渡して処理を「細工」するようなlambdaを返す。
なお、続けて>=で合成をチェーンできるように、戻り値として返すlambdaも同様に`lift`しておく。
これで準備はできた。
「計算の途中結果をputsで標準出力に吐く」細工を行うctxは、以下のように書く。
ctx = lambda{|g,x| # 引数の関数gを呼び出す res = g.call(x) # 結果を出力する puts "g(#{x}) -> #{res}" res }
では、実際に上記のctxをProc#liftメソッドに渡して、できあがった合成関数を呼び出してみよう。
# Proc#liftで関数合成に細工する i = f.lift(ctx) >= g >= h i.call([3,5]) # g(3) -> 4 # ctxから出力 # g(4) -> 8 # ctxから出力 # => 8
関数gと関数hの呼び出しの後に、標準出力へ結果が出力されていることがわかる。
これは、図にするとこのような感じだ。
先ほどの「nilならば計算を途中で打ち切る」細工は、ctxを以下のように定義すればいい。
ctx = lambda{|g, x| # nilならばgを呼び出さずにnilを返して後続の計算を打ち切る return x if x.nil? g.call(x) }
これで、先ほどと同じように動作する。
i = f.lift(ctx) >= g >= h i.call([4,8]) # => 10 i.call([]) # => nil
この細工を行う関数合成は、前回、前々回の内容を一般化したものだ。
「関数型Ruby」という病(4) - Applicativeスタイル(的ななにか) - ( ꒪⌓꒪) ゆるよろ日記
「関数型Ruby」という病(5) - Object#tryはMaybeモナドの夢を見るか? - ( ꒪⌓꒪) ゆるよろ日記
モナ……
さて、Proc#liftで「細工」を指定することで、様々な細工を関数合成に施すことができるようになった。
ここで、もう一度図を見直してみよう。
先ほどの「nilならば計算を途中で打ち切る」細工は、「失敗するかもしれない計算」という【文脈】上で関数合成が動作しているように見える
「標準出力に吐く」細工は、「結果を出力しながらする計算」という【文脈】上で関数合成が動作しているように見える。 あるいは、関数合成の下に「細工」が【配管】されているように見える。
「細工」を設定するメソッドを`lift`と名付けたのは、実は関数合成を【文脈】上に「持ち上げる」という意味を込めているからだ。
ここで上げたほかにも様々な文脈が考えられる。「外部から環境を与えられる文脈」「複数の結果を組み合わせる文脈」「非同期で計算する文脈」など……。
あれ、それってモナ……。おっと誰か来たようだ。
実際、モナ……則どころかモナ……の形すらしていない(returnもbindもない)のでモナ……ではないのだが、よくわからないと評判のアレも実はこういう配管をやるためのデザインパターンの一種である、と捉えると必要以上に恐怖を覚えずとも済む。
Proc#ymsr
なお、このProc#liftは拙作lambda_driver.gemに実装されており、liftは別名`ymsr`にaliasされている。
Aliased Proc#lift to Proc#ymsr · 953d5d9 · yuroyoro/lambda_driver · GitHub
java-ja.dddの最後の「関数型言語Dis(?)」について
java-ja.ddd - connpass
2013/03/22 java-ja.ddd #java_ja #java_ja_ddd - Togetter
java-ja.ddd面白かったです。色々と得るものがありました。発表者の方々、有り難うございました。
会場を提供して頂いたGREEさんありがとう。運営のみなさまお疲れ様。
で、一番最後に、「関数型言語をDisって逃げる」ということで発表者の一人である増田亨さんがごく短い時間お話しされました。
その内容があまりにも自分にとって納得しかねるものだったので、ブログにアウトプットします。
「関数型言語Dis」の中で、増田さんがされた主張は以下2点です。
関数型言語が、どの言語を指しているのかがまず不明ですが、そこは置いておいて。
まず一点目、「関数型言語ユーザーは業務の話ができない」について。
関数型言語ユーザー全員を指して、「業務の話ができない」という主張の根拠はそもそもどこにあるのでしょうか?
逆に「オブジェクト指向言語のユーザーは全員が業務についても堪能である」と一般化して主張してもよいのですか?
また、話の中で「関数型言語使いが書いた素晴らしいロジックを見たことがある」ともおっしゃっており、主張が一貫していません。
たまたま増田さんの周りの関数型言語ユーザーが業務について(そもそも業務ってなによ?ってのもあるけど)話ができないとしても、それを一般化して主張することの是非については、論ずるまでもないと思われます。
次に、「関数型言語は記号を多用しすぎる」について。
これについては、記号を多用することの弊害について踏み込んで語られておらず、単に「記号使いすぎるからねーハハハ」で終わっていて、「ああ、この人は記号が嫌いなんだな」ということは理解できても、そもそも関数型言語に対するDisにすらなっていないのが残念です。
記号を多用する言語(おそらくHaskellやScalaあたりの言語を指していると思われる)のユーザーにとっては、
記号で記述した方が自然で、同じ言語ユーザーに対してコードの意図を示しやすいことがあるという一面があります。
論理演算子(&&や||)は記号ですが、それを指して「記号だからダメだ!andとかorとかで書け!」という主張をする人は稀でしょう。
にも関わらず、関数型言語を使わない人がその記号の使い方について問題を感じるとしたら、そのような感覚を身につけているユーザーの母数が少ないとか、初学者に対して敷居が高いとか、ググラビリティが低いとかいう指摘になるでしょう。そこまでつっこんで話を聞くことができれば良かったのですが。
ともかく、短い時間でのお話で、議論にまで昇華できなかったのが残念でなりません。
機会があれば、双方の主張を議論しあえる場で、非関数型言語ユーザーから見た現状の関数型言語に対する問題点を提示して頂ければ、と思います。
Freeモナド in Scala
噂のFreeモナドをScalaで写経してみた。
Freeモナドは、取り込む型SのFunctorと組み合わせて、Functorの特性に応じたモナドを得ることができるものらしい。
そろそろFreeモナドに関して一言いっとくか - fumievalの日記
Freeモナドって何なのさっ!? - capriccioso String Creating(Object something){ return My.Expression(something); }
Haskell for all: Why free monads matter
stackless scala with free monad
独習 Scalaz: 18日目 | eed3si9n
( ゚д゚) 「ナンデ!?モナドナンデ!?」
モナドがセブンセンシズならFreeモナドはエイトセンシズくらいなのでかなり小宇宙を高めないと理解が追いつかないので、まずは写経してみた。
Freeモナドの定義
そのままScalaに移植するにあたって、型SはFunctorである必要があるので、Functor型クラスを用意し、flatMapにimplicit parameterでFunctor型クラスのインスタンスを注入する形式にする。
// type class: Functor trait Functor[F[_]] { def map[A, B](m: F[A])(f: A => B): F[B] } // data Free f a = Pure a | Free (f (Free f a)) // instance Functor f => Monad (Free f) where // return = Pure // Pure a >>= k = k a // Free fm >>= k = Free (fmap (>>=k) fm) f a)) // sealed trait FreeM[S[+_], +A] { def flatMap[B](f: A => FreeM[S, B])(implicit s: Functor[S]): FreeM[S, B] def map[B](f: A => B)(implicit s: Functor[S]): FreeM[S, B] = flatMap(a => Pure(f(a))) } case class Pure[S[+_], +A](a: A) extends FreeM[S, A] { def flatMap[B](f: A => FreeM[S, B])(implicit s: Functor[S]): FreeM[S, B] = f(a) } case class Free[S[+_], +A](k: S[FreeM[S, A]]) extends FreeM[S, A]{ def flatMap[B](f: A => FreeM[S, B])(implicit s: Functor[S]): FreeM[S, B] = Free(s.map(k)(_.flatMap(f))) }
haskellのをそのまま移植しただけで、特に難しいことはやってない。
サンプルのCharIOを写経する
そろそろFreeモナドに関して一言いっとくか - fumievalの日記 のCharIOを写経してみる。Haskell版ではIO[Char]になるが、ScalaにはIOモナド無いのでそのままCharでやる。
まずは、CharIO型の定義。これはそのまま。
// data CharIO a = GetCh (Char -> a) | PutCh Char a sealed trait CharIO[+A] case class GetCh[+A](f:Char => A) extends CharIO[A] case class PutCh[+A](c:Char, a:A) extends CharIO[A]
つぎに、CharIOのFunctorインスタンスを定義する。
// instance Functor CharIO where // fmap f (GetCh g) = GetCh (f . g) // fmap f (PutCh c x) = PutCh c (f x) implicit val charIOFunctor = new Functor[CharIO] { def map[A, B](a: CharIO[A])(f: A => B): CharIO[B] = a match { case GetCh(g) => GetCh(f compose g) case PutCh(c, x) => PutCh(c, f(x)) } }
これもそのまま。
続いて、Free[CharIO, Char]を返すユーティリティ関数を定義する。
// getCh :: Free CharIO Char // getCh = Free $ GetCh $ \ch -> Pure ch // // putCh :: Char -> Free CharIO () // putCh ch = Free $ PutCh ch (Pure ()) // def getCh:FreeM[CharIO, Char] = Free(GetCh({ch => Pure(ch)})) def putCh(ch:Char):FreeM[CharIO, Unit] = Free(PutCh(ch, Pure(())))
同じっすね。
では、サンプルどおりに、doの中でこれらの関数を用いてFree[CharIO, Char]を組み合わせてみる。
// mapM_ def mapFreeM[S[+_]:Functor, A](f:A => FreeM[S,Unit], s:Seq[A]):FreeM[S,Unit] = s.toList match { case x::xs => xs.foldLeft(f(x)){(m:FreeM[S,Unit],c:A) => m.flatMap{unit => f(c)} } case Nil => Pure(()) } // ex0 = do // mapM_ putCh "Hello, Haskeller! Please input a character:" // ch <- getCh // mapM_ putCh "The ordinal of the character is:" // mapM_ putCh (show (ord ch)) // mapM_ putCh ".\n Thank you!\n" } val io:FreeM[CharIO, Unit] = for{ _ <- mapFreeM(putCh, "Hello, Scala Programmer! Please input a character:") ch <- getCh _ <- mapFreeM(putCh, "The ordinal of the character is:") _ <- mapFreeM(putCh, ch.toInt.toString) _ <- mapFreeM(putCh, ".\n Thank you!\n") } yield ()
ScalaにはmapM_がないので、代わりにSeq[A]にそれぞれ(f: A => FreeM[S, Unit])を適用しつつflatMapでたたみ込むようmapFreeMを用意した。
で、Scalaにおけるモナド糖衣構文はforなので、これにFreeモナドを与えてみる。 "for{_ <- m ... } yield ()" って記法ェ……。
これで、Free[CharIO, Char]を結合させたもio:FreeM[CharIO, Unit]が得られた。これは、いわばSのFunctorによってflatMapで合成されたCharIOのLinkedListと言えなくもない。
あとは、このio:FreeM[CharIO, Unit]を渡り歩きながら処理を行うインタプリター関数を定義すればいいわけだ。
// runStdIO :: Free CharIO a -> IO a // runStdIO (Pure a) = return a // runStdIO (Free (GetCh f)) = getChar >>= \ch -> runStdIO (f ch) // runStdIO (Free (PutCh ch cont)) = putChar ch >> runStdIO cont def runCharIO(free:FreeM[CharIO, Unit]):Unit = free match { case Pure(a) => a case Free(GetCh(f)) => runCharIO(f(readChar())) case Free(PutCh(ch, cont)) => print(ch);runCharIO(cont) }
実行してみる。
scala> runCharIO(io)
Hello, Scala Programmer! Please input a character:The ordinal of the character is:97.
Thank you!
関数適用のススメ 〜 オブジェクト指向プログラマへ捧げる関数型言語への導入その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によって実現していますが、言語仕様としてこのような演算子を持つプログラミング言語が存在してもおかしくはありません。一般的なプログラミング言語では、()によって関数適用が表現されているだけ、とも言えます。
ようはなにが言いたいかというと
- 小さな関数を合成して大きな関数を作るのが、関数型のやり方
- 合成と適用を分けて考えると、合成した関数自体を保存しておいて、別な関数と合成したり、引数に渡したりできる
- その結果、カッコがネストしたコードを、左から読めるようなコードに書き換えることができる
次回は、再帰とラムダ計算の話をしようと思うゲソ。
関数合成のススメ 〜 オブジェクト指向プログラマへ捧げる関数型言語への導入その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
こうなります。