今日は、カリー化と関数の部分適用の話題。
Haskellの視座からScalaのカリー化と部分適用を見てみる
まず、Haskellでの関数のカリー化と部分適用についておさらい。
例えば、引数を合計する関数 mysum があるとして、
mysum :: Num a => a -> a -> a -> a mysum a b c = a + b + c
次のようにすると当然期待値を得ることができます。まぁ当然ですね...。
ghci> mysum 1 2 3 6
しかし、このコードをJava脳を用いて読んだ場合、
mysum(1,2,3)
のように読んでしまいがちですが、次のように読むのが正解です。
((mysum 1) 2) 3
Haskellでは関数は必ず一つの引数を取るという考え方があり、2個目の引数は1つ目の引数を取る関数の引数として渡されます。3つ目以降も同様です。これを関数がカリー化されているといいます。
このmysum
関数で考えてみると、mysum
はa -> (a -> a -> a)
です。つまり引数を一つ取り、(a -> a -> a)という関数を戻り値として返す関数と言える。
また、引数が部分的用されたmysum 1
はa -> (a -> a)
です。つまり引数を一つ取り、(a -> a)という関数を戻り値として返す関数。
引数が部分的用されたmysum 1 2
はa -> a
です。つまり引数を一つ取り計算結果を戻り値として返す関数、ということが言えます。
ghci> :t mysum mysum :: Num a => a -> a -> a -> a ghci> :t mysum 1 mysum 1 :: Num a => a -> a -> a ghci> :t mysum 1 2 mysum 1 2 :: Num a => a -> a
この仕組みを利用すると、引数を部分適用した関数を得て、残りの引数を後で適用できるわけです。こんな感じ。
ghci> let mysum_1 = mysum 1 ghci> mysum_1 2 3 6
Scalaでもカリー化と引数の部分適用ができます。
話が逸れますが、Scalaは引数のリストを複数個定義できます。
scala> def sum(a:Int)(b:Int)(c:Int):Int = a + b + c
sum: (a: Int)(b: Int)(c: Int)Int
引数を部分適用せずに普通に呼び出すにはこんな感じ。
scala> sum(1)(2)(3) res0: Int = 6
部分適用した関数が欲しければ、次のようにすればよいです。*1
scala> val sum_1 = sum(1) _ sum_1: Int => (Int => Int) = <function1> scala> sum_1(2)(3) res0: Int = 6
sum_1はカリー化されているのですが、sum自体はカリー化されていません。Int => Int => Int => Int
という型の関数になっていないからですね。ややこしいのですが、sumは複数の引数リストを持っているだけで、カリー化されているわけでないってことですね。
次のようにするとカリー化されたメソッドを定義できます*2。
scala> def sum(a:Int) = (b:Int) => (c:Int) => a + b + c
sum: (a: Int)Int => (Int => Int)
また、次のようなカリー化されていない関数をカリーしたい場合はcurriedメソッド*3が使えます。
scala> def sum(a:Int, b:Int, c:Int):Int = a + b + c scala> val sum_curried = (sum _).curried sum_crried: Int => (Int => (Int => Int)) = <function1>
Haskellの関数定義と同じような形式の関数に変換されました。すべての引数をFunction1型関数の組み合わせで処理する構造に変わりました。 Haskellと同様に引数に部分適用した関数に残りの引数を適用すれば計算結果が得られます。
scala> val sum_1 = sum_curried(1) sum_1: Int => (Int => Int) = <function1> scala> sum_1(2)(3) res0: Int = 6
Dxoでの実例
で、どういうところで使えるわけ?って声が聞こえてきそうなので、Scala(Scalaz)での実例を考えてみました。
ドメインの異なるオブジェクトを相互に変換するためのオブジェクトである、Dxoの実例を紹介します。
DxoはシンプルなFunctorの実装で、Dxoの値(value)にmapの引数に与えた関数fを適用するだけです。適用するとfによって変換されたオブジェクトが返ってきます。 ただし、その関数はf : A => Bでなければなりません。このコード例は、UserAからUserBへの変換を示してします。
import scalaz._ import Scalaz._ case class UserA(name:String) case class UserB(fname:String, lname:String) case class Dxo[A](value:A) object Dxo { implicit object Functor extends Functor[Dxo] { def map[A, B](fa: Dxo[A])(f: A => B): Dxo[B] = { Dxo(f(fa.value)) } } } object Main extends App { val create = {a:UserA => val names = a.name.split(":"); UserB(names(0), names(1))} val append = {(b:UserB, a:UserA) => UserB(b.fname+":"+a.name, b.lname+":"+a.name)}.curried val userB1 = Dxo(UserA("junichi:kato")) map create val userB2 = Dxo(UserA("junichi:kato")) map append(UserB("hoge", "fuga")) println(userB1.value) println(userB2.value) }
create関数は引数にUserAを取って、UserA#nameを":"で分割し、UserBに変換して返す関数です。関数の型はUserA => UserB
です。
val create = {a:UserA => val names = a.name.split(":"); UserB(names(0), names(1))} val userB1 = Dxo(UserA("junichi:kato")) map create
と、ここまでやってることは具体的な変換処理をcreate関数に定義して呼び出しているだけなので単純です。
ここからカリー化と部分適用の出番です。
Dxo#mapはUserAからUserBを得ることなっているのですが、手元にすでにUserBのインスタンスがある場合は(b:UserB, a:UserA) => UserB
の形式の関数を考えたくなります。
この例のappend関数は、適当に既存のUserBとUserAを組み合わせて新しいUserBを生み出す関数ですが、このままじゃmapメソッドに渡すことができません。UserA => UserB
の型でなければなりません。
そこでcurriedでカリー化するわけですね。
カリー化した関数に第一引数であるUserBを部分適用します。そうすると、append(UserB("hoge", "fuga"))
は部分適用された関数なので、残りの引数はUserAのみとなるので関数の型はUserA => UserB
となるわけです*4。map処理の呼び出しは、なんとなくDSLっぽく見えてよい感じですね。
val append = {(b:UserB, a:UserA) => UserB(b.fname+":"+a.name, b.lname+":"+a.name)}.curried val userB2 = Dxo(UserA("junichi:kato")) map append(UserB("hoge", "fuga"))
カリー化した関数の部分適用は、関数の抽象化のためのひとつの手法ということになりますかね。