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

かとじゅんの技術日誌

技術の話をするところ

関数のカリー化と部分適用

今日は、カリー化と関数の部分適用の話題。

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関数で考えてみると、mysuma -> (a -> a -> a) です。つまり引数を一つ取り、(a -> a -> a)という関数を戻り値として返す関数と言える。 また、引数が部分的用されたmysum 1a -> (a -> a)です。つまり引数を一つ取り、(a -> a)という関数を戻り値として返す関数。 引数が部分的用されたmysum 1 2a -> 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"))

カリー化した関数の部分適用は、関数の抽象化のためのひとつの手法ということになりますかね。

*1:sumはメソッドなので、関数化するときに_をつけてあげる必要があります

*2:メソッドの型がちょっと想定と異なりますが、一応カリー化されています

*3:Function2から22で定義されているメソッド

*4:append関数が(a:UserA,b:UserB) => UserBという形式の場合はカリー化して部分適用しても関数の型が意図通りにならないので注意が必要