カリー化ってなんだろう?

こんにちはKUJIRAです。今日は以前、クロージャの記事で少し触れたカリー化について話したいと思います。( ちなみに画像は定番のネタです。今回の話とは全く関係ありません )
これも普通に手続き型のプログラムを組んでいると中々使う機会のないもので、理解するまでには時間がかかると思います。Wikipediaにも数式とか書いてあって最初から読むのを諦めるレベルです。
Wikipediaの説明を読んでみる
我らのWikipediaには以下のように記載されています。
カリー化 (currying, カリー化された=curried) とは、複数の引数をとる関数を、引数が「もとの関数の最初の引数」で戻り値が「もとの関数の残りの引数を取り結果を返す関数」であるような関数にすること(あるいはその関数のこと)である。
はい、また訳のわからない日本語が炸裂しております。
複数の引数をとる関数を〜
前提はこれ。これが、
引数が「もとの関数の最初の引数」で戻り値が「もとの関数の残りの引数を取り結果を返す関数」であるような関数にすること(あるいはその関数のこと)
こうなる訳です。正直言って前提条件しか分かりません。マジで日本語でおk。
まず、この文章を分からなくしているのが 「もとの関数」 というワードです。
「もとの関数の最初の引数」が引数 で、 「もとの関数の残りの引数をとって結果を返す関数」が戻り値 になるようにしなければいけないらしいです。
ちょっと例を示しながら説明します。例えば複数の引数を持つ関数があります。( 例によって今回もScalaのコードです )
1 |
def testMethod(firstStr: String, secondStr: String): String = s"$firstStr:$secondStr" |
これを もとの関数 とした時、 「最初の引数」が引数 ということは二つ目以降の引数に目を瞑ると以下のようになります。
1 |
def testMethod(firstStr: String) = ??? // 一回、2つ目の引数については忘れてね |
で、これに対して 「元の関数の残りの引数をとって結果を返す関数」が戻り値 になるようにすると以下のようになります。
1 2 3 |
def testmethod(firstStr: String): String => String = { (secondStr: String) = s"$firstStr:$secondStr" } |
これがカリー化です。最初の引数(上記のfirstStr)を引数にとり、2番目の引数をとって結果を返すメソッドをリターンしています。
「おいおい、ちょっと待ってくれよ。これちゃんと同じ結果になるの?」と思う方もいらっしゃるかもしれません。なぜならこの記述だと、firstStrとsecondStrが全く別スコープにあるような感じがするからです。そこで思い出して欲しいのが前回のクロージャの記事です。 クロージャが解決できる変数はクロージャが引数で受け取った値と、クロージャ内で定義された値、および、クロージャの外で定義された値でした。 なので、この定義は結果として出力される結果は変わりません。
さて、こんな定義のされ方をしているこのメソッド、一体どうやって実行するんだよ?ともなると思います。このメソッドはScalaでは以下のように実行します。
1 2 3 4 5 6 7 8 9 |
object Test { def main(args: Array[String]) = { println(testMethod("test1")("test2")) // <= この部分がメソッド実行部分 } def testMethod(firstStr: String): String => String = { (secondStr: String) => s"$firstStr:$secondStr" } } |
メソッドに対して区切られた形で引数を渡しています。一つ目を引数で渡した後、無名関数が返ってくるので、それにさらに引数を渡している感じを表現している・・・気がしますw
上記のコード、実はさらに簡略化することができます。
1 2 3 4 5 6 7 |
object Test { def main(args: Array[String]) = { println(testMethod("test1")("test2")) } def testMethod(firstStr: String)(secondStr: String) = s"$firstStr:$secondStr" } |
こっちの実装の方が、実際に使用する時と形が似ているのでしっくりくると思います。
さて、ここまでカリー化について話しましたが、ここでカリー化を行う上でよく起こる勘違いについて話したいと思います。よくする勘違いとしては、元のメソッドが複数の引数をとってしまうことです。
例えば、
1 2 3 |
def testmethod(firstStr: String, secondStr: String) = { (thirdStr: String) = s"$firstStr:$secondStr:$thirdStr" } |
というのはカリー化ではないです。冒頭に載せたカリー化の定義ですが、意味としては 「複数の引数をとる関数」に対して「最初の引数を引数」にとり「残りの引数をとって結果を返す関数」を戻り値にする でした。
なので、元の関数からカリー化を行う際は引数が一つになるようにしなければいけません。上の例だと引数を複数とっているためカリー化にはならないということが分かります。逆に以下はカリー化ができている状態です。
1 2 3 |
def testmethod(firstStr: String) = { (secondStr: String, thirdStr: String) = s"$firstStr:$secondStr:$thirdStr" } |
ただ、通常は一回カリー化したらとことんカリー化する場合が多いので、以下のように内包している無名関数もカリー化してしまいます。
1 2 3 4 5 |
def testmethod(firstStr: String) = { (secondStr: String) => { (thirdStr: String) => s"$firstStr:$secondStr:$thirdStr" } } |
どういう使い方をするのか?
以上までのことを踏まえ、実際にKUJIRAがどう使っているかについて話したいと思います。
KUJIRAはこのカリー化を使う時には引数の部分適用を利用したり、クロージャと併用して利用することが多いです。以下に例を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
object ClosuerTest2 { def main(args: Array[String]) = { print(s"北海道:$getHokaido, 関東:$getKanto") } def getKanto: Int = { // 関東地方の県を出力するためのフィルタ val f: List[String] => List[String] = argList => { val kantoList: List[String] = List("茨城県", "栃木県", "群馬県", "埼玉県", "千葉県", "東京都", "神奈川県") println("関東地方一覧:") argList.filter(kantoList.contains) } // 共通のファンクションにフィルタを投入 val count: Int = printAndCountListWithFilter(f) println(s"関東地方の都道府県の数:$count") count } def getHokaido: Int = { // 北海道地方の都道府県を出力するためのフィルタ val f: List[String] => List[String] = argList => List(argList.head) // 共通のファンクションにフィルタを投入 val count: Int = printAndCountListWithFilter(f) println(s"北海道の都道府県の数:$count") count } def printAndCountListWithFilter(filter: List[String] => List[String]): Int = { val resultList: List[String] = filter(List( "北海道", "青森県", "岩手県", "宮城県", "秋田県", "山形県", "福島県", "茨城県", "栃木県", "群馬県", "埼玉県", "千葉県", "東京都", "神奈川県", "新潟県", "富山県", "石川県", "福井県", "山梨県", "長野県", "岐阜県", "静岡県", "愛知県", "三重県", "滋賀県", "京都府", "大阪府", "兵庫県", "奈良県", "和歌山県", "鳥取県", "島根県", "岡山県", "広島県", "山口県", "徳島県", "香川県", "愛媛県", "高知県", "福岡県", "佐賀県", "長崎県", "熊本県", "大分県", "宮崎県", "鹿児島県", "沖縄県" )) resultList.foreach(println) resultList.size } } |
これは以前、クロージャの記事で書かせていただいたサンプルです。ここで出てくる printAndCountListWithFilterメソッド をカリー化を利用してもう少し汎用的なメソッドに変えてみたいと思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
object ClosuerTest2 { val PREFECTURE_LIST: List[String] = List( "北海道", "青森県", "岩手県", "宮城県", "秋田県", "山形県", "福島県", "茨城県", "栃木県", "群馬県", "埼玉県", "千葉県", "東京都", "神奈川県", "新潟県", "富山県", "石川県", "福井県", "山梨県", "長野県", "岐阜県", "静岡県", "愛知県", "三重県", "滋賀県", "京都府", "大阪府", "兵庫県", "奈良県", "和歌山県", "鳥取県", "島根県", "岡山県", "広島県", "山口県", "徳島県", "香川県", "愛媛県", "高知県", "福岡県", "佐賀県", "長崎県", "熊本県", "大分県", "宮崎県", "鹿児島県", "沖縄県" ) val printAndCountListWithFilter = (list: List[String]) => (filter: List[String] => List[String]) => { val resultList: List[String] = filter(list) resultList.foreach(println) resultList.size } def main(args: Array[String]) = { val curryFunc = printAndCountListWithFilter(PREFECTURE_LIST) println(s"北海道:${getHokaido(curryFunc)}, 関東:${getKanto(curryFunc)}") } def getKanto(logic: (List[String] => List[String]) => Int): Int = { // 関東地方の県を出力するためのフィルタ val f: List[String] => List[String] = argList => { val kantoList: List[String] = List("茨城県", "栃木県", "群馬県", "埼玉県", "千葉県", "東京都", "神奈川県") println("関東地方一覧:") argList.filter(kantoList.contains) } // 共通のファンクションにフィルタを投入 val count: Int = logic(f) println(s"関東地方の都道府県の数:$count") count } def getHokaido(logic: (List[String] => List[String]) => Int): Int = { // 北海道地方の都道府県を出力するためのフィルタ val f: List[String] => List[String] = argList => List(argList.head) // 共通のファンクションにフィルタを投入 val count: Int = logic(f) println(s"北海道の都道府県の数:$count") count } } |
ポイントは前回まで普通の関数で定義されていた printAndCountListWithFilter をクロージャにしているところです。変数に格納しているのはカリー化された無名関数です。
ここで興味深いのが無名関数の書き方です。
1 2 3 4 5 |
val printAndCountListWithFilter = (list: List[String]) => (filter: List[String] => List[String]) => { val resultList: List[String] = filter(list) resultList.foreach(println) resultList.size } |
ここでの無名関数の書き方は先ほど記載した 簡略化する前の「testMethod」 の書き方に準じています。簡略した書き方だと無名関数では使えないので注意が必要です。
この実装だと、「printAndCountListWithFilter」はフィルターをかける対象となる文字列のリストを任意に設定することができるので、以下のように友達のリストから特定の名前に該当する人を抽出することも可能です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
object CurryTest { val FRIEND_LIST: List[String] = List( "てすと太郎", "花岡テス二郎","佐々木テス子", "てすと二郎" ) val printAndCountListWithFilter = (list: List[String]) => (filter: List[String] => List[String]) => { val resultList: List[String] = filter(list) resultList.foreach(println) resultList.size } def main(args: Array[String]): Unit = { val curryFunc = printAndCountListWithFilter(FRIEND_LIST) getTest(curryFunc) } def getTest(logic: (List[String] => List[String]) => Int): Int = { // 北海道地方の都道府県を出力するためのフィルタ val f: List[String] => List[String] = argList => argList.filter(friendName => friendName.contains("てすと")) // 共通のファンクションにフィルタを投入 val count: Int = logic(f) println(s"抽出した友達の数:$count") count } } |
このプログラムの実行は以下の通りです。
てすと太郎 てすと二郎 抽出した友達の数:2
まとめ
今日はカリー化について書きました。カリー化って結局のところ
- メソッドを返すメソッド
- カリー化対象のメソッドは引数を一つしかとらない
- カリー化は部分適用と併用すると便利に使える
に尽きると思います。クロージャやカリー化、部分適用などの機能を使って効率よくプログラムが組めるととても楽しいので、この記事を読んで少しでも理解して体得への手がかりにしていただければ幸いです。
それでは今日はここまで。