Scala2.10.0のDependent method typesと型クラスを組み合わせた『The Magnet Pattern』がヤバい件
これが……型の力かッ……!!
spray | Blog » The Magnet Patternという記事で、「The Magnet Pattern」というデザインパターンが紹介されている。
これは、メソッドオーバーロードで解決していた問題を、型クラスとDependent method typesを組み合わせて置き換えることで、オーバーロードの際の様々な制約(Type Erasureなど)を突破し、より柔軟な拡張性を得ることができるというもの。このパターンでは、引数の型に応じて異なる結果型を返すようにできる。
この記事で、今まで何のために使われるのかわからんかったDependent method typesの有効性が理解でき、あらためて型の力を思い知った。
以前に"Generalized type constraints"(Scalaで<:<とか=:=を使ったgeneralized type constraintsがスゴすぎて感動した話 - ( ꒪⌓꒪) ゆるよろ日記) を知った時以来の感動だったので、勢いで書いてみた。
Dependent method types ってなんぞ?
簡単に言うと、引数の値に応じて結果型が変わるメソッドを定義できるということ。
Scala 2.10 に dependent method types というのが入るらしいよ - scalaとか・・・
以下の例では、fメソッドは、引数のFooトレイトのResult型に応じて結果型が変わる
trait Foo{ type Result def bar:Result } // 引数のFooトレイトのResult型に応じて結果型が変わる def f(obj:Foo):obj.Result = obj.bar
ResultをStringで定義してるstringFooオブジェクトと、Intで定義しているintFooオブジェクトを用意し、
val stringFoo = new Foo { type Result = String def bar:Result = "foo" } val intFoo = new Foo { type Result = Int def bar:Result = 99 }
fメソッドにそれぞれ渡すと、引数の値に応じて結果型が変わっていることがわかる。もちろん型安全なので、全てコンパイル時に型チェックが行われる。
scala> f(stringFoo) res0: stringFoo.Result = foo scala> f(intFoo) res1: intFoo.Result = 99 scala> f(stringFoo).getClass res2: Class[_ <: stringFoo.Result] = class java.lang.String scala> f(intFoo).getClass res3: Class[intFoo.Result] = int
The Magnet Patternが解決する問題
引数の型に応じてメソッドの振る舞いを変えたい場合、一つの手段としてメソッドをオーバーロードする、という手がある。 しかし 元記事では、オーバーロードでは以下のような問題が発生する、と述べている。
- type erasureにより引数の型が衝突する場合がある
- メソッドからFunctionオブジェクトに"lift"できない(println _のような)
- package objectでは使えない(2.9以前)
- 似たようなコードが多発
- デフォルト引数の利用に制限がある
- 引数の型推論に制限がある
The Magnet Patternは、この問題のいくつかを解決し、さらなるメリットをもたらす
- type erasureによる型の衝突は解決
- Functionオブジェクトへのliftは、全ての結果型が同一であれば可能
- package objectでも定義できる
- 実装をDRYにできる
- 危険なimplicit conversionの定義を避けることが出来る
- 引数の型に応じて異なる結果型を返すことが可能
このパターンは、拡張性に柔軟をもたらす一方で、DrakSideもあると。
- オーバーロードに比べて実装が細かく見通しが悪くなる
- 名前付きパラメータは利用できない
- by-name(名前渡し)パラメータとimplict conversionの組み合わせで重複してby-nameパラメータが評価されることがある
- Magnet Parttenで定義するメソッドは引数宣言が必須
- デフォルト引数は定義できない
- 引数に対しての型推論はできない(引数に無名関数を渡す場合などで)
The Magnet Patternの例
元記事では、例としてSprayにおけるURLルーティングを定義するDSLの実装をあげている。 非常にわかりやすいのでそっちを見てもらうのがいいのだけど、それだとあんまりなので例を書いてみる。
ここでの例は、3個のIntまたはStringから日付変換を行う関数toDateを考える。
通常のオーバーロードでの実装
object M { def toDate(year:Int, month:Int, date:Int): java.util.Date = { val c = java.util.Calendar.getInstance c.set(year, month - 1, date, 0, 0, 0) c.getTime } def toDate(s:String): java.util.Date = (new java.text.SimpleDateFormat("yyyy/MM/dd")).parse(s) }
まぁ見たとおりです。
scala> M.toDate(2013,1,21) res40: java.util.Date = Mon Jan 21 00:00:00 JST 2013 scala> M.toDate("2013/01/21") res41: java.util.Date = Mon Jan 21 00:00:00 JST 2013
The Magnet Patternで書き換えてみる
The Magnet Patternでは、オーバーロードを行う代わりに、メソッドの引数にある型(仮にmagnet型と呼ぶ)を一つだけとるようにする。 そして、そのmagnet型へのimplicit convesionを定義することで、オーバーロードと同様に引数の型に応じた振る舞いを定義できる。
さらに、The Magnet Patternでは引数の型に応じて結果型を変えることが可能である。 "magnet型"に抽象型で結果型を持たせ、Depenent method typesを利用して、引数がIntの場合はDateではなくCalndarを返すようにしている。
scala> :paste // Entering paste mode (ctrl-D to finish) // toDateは引数に"magnet型"を取るようにする。変換の実装はconvertメソッドに任せる // 結果型は、"magnet型"が持つ抽象型Resultに依存させている def toDate(magnet:DateMagnet):magnet.Result = magnet.convert // toDate関数で使われる"magnet型"のtrait trait DateMagnet{ type Result // 変換結果の結果型は抽象型で持つ def convert():Result // 変換を行うメソッド } // "magnet型"のコンパニオンオブジェクトに、 // それぞれの引数の型に合わせた"magnet型インスタンス"を返すimplicit defを // 定義しておく object DateMagnet { // 3つのIntからDateMagnetのインスタンスへ implicit def fromInt(tuple:(Int, Int, Int)) = new DateMagnet { type Result = java.util.Calendar def convert():Result = { val (year, month, date) = tuple val c = java.util.Calendar.getInstance c.set(year, month - 1, date, 0, 0, 0) c } } // StringからDateMagnetのインスタンスへ implicit def fromString(s:String) = new DateMagnet { type Result = java.util.Date def convert():Result = (new java.text.SimpleDateFormat("yyyy/MM/dd")).parse(s) } } // Exiting paste mode, now interpreting. warning: there were 2 feature warnings; re-run with -feature for details toDate: (magnet: DateMagnet)magnet.Result defined trait DateMagnet defined module DateMagnet scala> toDate(2013, 1, 21) res0: java.util.Calendar = java.util.GregorianCalendar[time=?,areFieldsSet=false,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="Asia/Tokyo",offset=32400000,dstSavings=0,useDaylight=false,transitions=10,lastRule=null],firstDayOfWeek=1,minimalDaysInFirstWeek=1,ERA=1,YEAR=2013,MONTH=0,WEEK_OF_YEAR=4,WEEK_OF_MONTH=4,DAY_OF_MONTH=21,DAY_OF_YEAR=25,DAY_OF_WEEK=6,DAY_OF_WEEK_IN_MONTH=4,AM_PM=1,HOUR=10,HOUR_OF_DAY=0,MINUTE=0,SECOND=0,MILLISECOND=88,ZONE_OFFSET=32400000,DST_OFFSET=0] scala> toDate("2013/01/21") res1: java.util.Date = Mon Jan 21 00:00:00 JST 2013
実装のポイントは、
- 引数は"magnet型"を一つとるようにする
- implicit conversionで引数の型に応じた"magnet型"のインスタンスへ変換する
- 型に応じた振る舞いは、"magnet型"のメソッドに実装する
- Dependent method typesを利用することで、"magnet型"の抽象型を差し替えて結果型を変えることができる
上記4点である。ひとことでいうと、オーバーロードで実装されている型毎の処理を型クラスに移した、といえる。
まとめ
メソッドのオーバーロードでは、Type Erasureによる制約などがあるが、 The Magnet Patternではimplicit convesionを定義することであとで対応する型を増やしたり、 結果型を変えたりと、様々な柔軟性を得ることができる。 一方で、実装が複雑になる、コードの見通しが悪くなる、などのデメリットもある。
興味深いのは、このThe Magnet Patternは型クラスやDepenent method typesを利用した、従来のOOPでは実現できない新しいプログラミングパラダイムにおけるデザインパターンであるという事実だ。
デザインパターンもGoFからまだまだ進化していると言える。
これが……型の力だッッッ!!!!
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!
Scalaにおける型パラメータの部分適用 [({type F[X] = G[A,X]})#F] について
Tumblrから出戻ってきました。
8/4のLL DecadeのLT大会に出るのでぜひお越しください。
さて、モナってますか? scalazなどでよく出現する[({type F[X] = G[A,X]})#F]のようなコードですが、これが何を意味しているのか最近やっと理解できたので、久しぶりにScalaの事書きます。
この記事はhigher kinded type(高階型)を理解していることが前提です。
結論からいうと、型パラメータの部分適用を行うためのテクニックです。以下のサンプルコードはscalazを使ってます。
用語
まず、この記事で使う用語を定義します。
- higher kinded type(高階型)
- 「いくつかの型パラメータを取る型コンストラクタ」のこと。
- 例えば、ListはList[Int]のように、「型パラメータをひとつ取る型」なので高階型。
- Eitherは、Either[Throwable, String]のように「型パラメータを二つとる」高階型。
- 高階型を型パラメータに取る関数は、"def foo[F[_,_]]"のように定義する。この例では、Fは「型パラメータを二つとる」高階型。
- kind
- 高階型が、いくつの型パラメータを取るかを表す。いわば、型コンストラクタのシグニチャ。
- Option, Listのような「型パラメータを一つ取る高階型」は、kindを「* -> *」と表記する
- Map, Eitherのような「型パラメータを二つ取る高階型」は、kindを「* -> * -> *」と表記する
- 高階型を型パラメータにとるような関数を呼び出すときには、kindが一致する型を渡さなければならない。上記の例のfoo関数には、kindが"* -> *_-> *"である高階型を渡さないとコンパイルエラーとなる
- 高階型ではない普通の型(StringとかIntとか)のkindは"*"。List[Int]のように、高階型に具体的な型を与えた型のkindも、同じく"*"
基本編
では、サンプルコードです。scalazには、pureというメソッドがあります。これはkindが1(* -> *)の高階型でかつPureのインスタンスである型を引数にとり、値を包み込む関数です。
import scalaz._;import Scalaz._ scala> 1.pure[Option] res1: Option[Int] = Some(1) scala> "foo".pure[List] res2: List[java.lang.String] = List(foo) scala> "oppai".pure[Either] <console>:14: error: Either takes two type parameters, expected: one "oppai".pure[Either] ^ <console>:14: error: kinds of the type arguments (<error>) do not conform to the expected kinds of the type parameters (type F). <error>'s type parameters do not match type F's expected parameters: <none> has no type parameters, but type F has one "oppai".pure[Either]
pure[Either]がエラーになっているのは、"error: Either takes two type parameters, expected: one"というエラーメッセージが示すとおり、pureはkindが「* -> *」である型を期待しているにも関わらず、「* -> * -> *」である型が渡されたからだと分かります。
ここで、pureを利用して、LeftがStringであるEitherに値を包みたいとします。どう書けばいいでしょうか?
scala> 1.point[Either[String,_]] <console>:17: error: Either[String, _] takes no type parameters, expected: one 1.point[Either[String,_]] scala> 1.point[Either[String,Int]] <console>:17: error: Either[String,Int] takes no type parameters, expected: one 1.point[Either[String,Int]
pureには、kindが「* -> *」である型コンストラクタを渡す必要があります。pureからLeftがStringでRightが任意に変わるような、「* -> *」な型コンストラクタが必要です。このようにすれば解決します。
scala> type StringEither[A] = Either[String,A] defined type alias StringEither scala> 1.pure[StringEither] res42: StringEither[Int] = Right(1)
typeを利用して、型パラメータを一つ取って、Either[String, A]を返す型コンストラクタ(StringEither[_])を定義しました。このStringEitherのkindは「* -> *」なので、pureに渡すことができます。解決です。
ですが、いちいちtypeで型を定義してからpureを呼ぶ必要があるのは面倒です。Listのmapやfilterのような高階関数を呼び出すときに、いちいち関数を別な変数に束縛してから呼び出す、なんてことはふつうはしないですよね?
型コンストラクタでも、無名関数のように「その場で使い捨てる型コンストラクタ」を定義できれば便利です。
[({type F[X] = G[A,X]})#F] は、まさしくそのためのイディオムなのです。なんだってーーー!!!!!
scala> 1.pure[({type F[A] = Either[String,A]})#F] res44: scala.util.Either[String,Int] = Right(1)
pureには({type F[A] = Either[String,A]})#F、という無名型コンストラクタ(造語です。本当にそう呼ぶかはわかりません)を渡してます。この無名型コンストラクタでは、型パラメータを一つとってEither[String, A]を返す型婚ストラクタFを動的に定義して、そのFがpureに渡されているので、kindも一致して問題なくpureが動作します。
応用編
さて、このような動的な型コンストラクタの定義を、関数が型パラメータを受け取る際に行うことがでます。
例として、任意の型A,Bを受け取って、pureを利用してEitherに包み込む関数pureEitherを定義します。
scala> def pureEither[A,B](v:B) = v.point[({type F[X] = Either[A,X]})#F] pureEither: [A, B](v: B)scala.util.Either[A,B]
このpureEitherでは、型パラメータAとBから、EitherにAを部分適用した無名型コンストラクタFをpureに渡すことで、A, Bからpureを利用してEitherに包むことを実県しています。
scala> pureEither[String,Int](1) res49: scala.util.Either[String,Int] = Right(1) scala> pureEither[Throwable,Boolean](false) res50: scala.util.Either[Throwable,Boolean] = Right(false)|scala|
以上。
関数適用のススメ 〜 オブジェクト指向プログラマへ捧げる関数型言語への導入その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
こうなります。
Scalazを簡単に試してみるための"scalaz-playground"というプロジェクトを作った
yuroyoro/scalaz-playground · GitHub
タイトルの通りで、このリポジトリをcloneして、"sbt console"と入力汁。
ozaki@mbp-4 $ git clone https://github.com/yuroyoro/scalaz-playground.git [~/sandbox/.../yuroyoro/work] Cloning into 'scalaz-playground'... remote: Counting objects: 7, done. remote: Compressing objects: 100% (6/6), done. remote: Total 7 (delta 0), reused 7 (delta 0) Unpacking objects: 100% (7/7), done. ozaki@mbp-4 $ cd scalaz-playground/ [~/sandbox/.../yuroyoro/work] ozaki@mbp-4 $ sbt clone (git)-[master][~/sandbox/.../work/scalaz-playground] Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8 ozaki@mbp-4 $ sbt console (git)-[master][~/sandbox/.../work/scalaz-playground] Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8 [info] Loading project definition from /Users/ozaki/dev/Project/sandbox/scala/yuroyoro/work/scalaz-playground/project [info] Updating {file:/Users/ozaki/dev/Project/sandbox/scala/yuroyoro/work/scalaz-playground/project/}default-48e6aa... [info] Done updating. [info] Set current project to scalaz playground (in build file:/Users/ozaki/dev/Project/sandbox/scala/yuroyoro/work/scalaz-playground/) [info] Updating {file:/Users/ozaki/dev/Project/sandbox/scala/yuroyoro/work/scalaz-playground/}default-2e3acd... [info] Done updating. [info] Starting scala interpreter... [info] import scalaz._ import Scalaz._ Welcome to Scala version 2.9.1.final (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_29). Type in expressions to have them evaluated. Type :help for more information. scala> false !? Option("あばばばばば") res1: Option[java.lang.String] = Some(あばばばばば) scala> false ?? Option("あばばばばば") res2: Option[java.lang.String] = None