カリー化に関する覚え書き

no extension

どこぞの某ウイルスのせいでメチャメチャ忙しい。 しかもここ1ヶ月くらいですっかり脅威扇動型ビジネス・モデルへと変貌したようで,ホンマにいい迷惑である。 もはやため息しか出ない。 ので,この件は無視することに決めた。

と,まぁ近況はこれくらいにして,今回は「カリー化」の話。 いや,関数型プログラミング言語への馴染みが薄いせいですぐ忘れちゃうのよ。

というわけで,覚え書きとして記しておく。

カリーは 飲みもの 動詞

Wikipedia によると「カリー化(currying)」とは

複数の引数をとる関数を、引数が「もとの関数の最初の引数」で戻り値が「もとの関数の残りの引数を取り結果を返す関数」であるような関数にすること
カリー化 - Wikipediaより

とある。 「カリー」は偉い数学者である Haskell B. Curry の名前から拝借したものらしい。 名前が動詞化しちゃってるのね(笑)

詳しくは近所の数学オタクに訊きなはれ。

関数型言語におけるカリー化

ガチの関数型プログラミング言語 Haskell ではカリー化は言語仕様に組み込まれていて,たとえば関数 add の定義

add x y = x + y

は実際にはカリー化表現

add = \x -> \y -> x + y

の糖衣構文となっている1

カリー化のメリットは関数の部分適用(partial application)が作れることで2,たとえば

increment = add 1

とすれば add を実引数 1 で部分適用とした新しい関数 increment をシンプルに記述できる。 もちろん,わざわざ名前を付けなくても無名関数として使えばいいのだが。

関数型じゃなくてもカリー化はできる

ガチの関数型プログラミング言語じゃなくても第一級関数(first-class function)をサポートするプログラミング言語であればカリー化の記述自体は可能である。

たとえば Go 言語なら

package main

import "fmt"

func add(x int) func(int) int {
	return func(y int) int {
		return x + y
	}
}

func main() {
	fmt.Println(add(1)(2)) //Output: 3
	increment := add(1) //partial application
	fmt.Println(increment(2)) //Output: 3
}

のように書ける。 JavaScript でも

function add(x) {
    return function(y) {
        return x + y;
    };
}

console.log(add(1)(2)); //Output: 3
let increment = add(1); //partial application
console.log(increment(2)); //Output: 3

と書くことができる。 さらに JavaScript ではアロー関数式が使えるので,関数 add の定義を

const add = x => y => x + y;

などと書くことも可能である。 ここまでくると,だいぶ関数型っぽいよね。

「それができる」ことと「そのように作られている」ことには天と地ほどの違いがある

この記事を書いて思い出したが,随分前に脊髄反射で

と呟いた。 今回の話はまさにそれ。

まぁ,そもそも Go 言語の場合はシンプルを旨とする思想な上に構文(statement)による制約が強いため,関数型っぽい記述には(書けるとしても)向いてない。

JavaScript は ES5 以降から関数型の要素を大幅に取り込んでいるが, Haskell と比較すれば分かるとおり,「関数」に対する考え方の根本が異なっている。

これは良し悪しの問題ではない。 まさに「制約は構造を生む」で,そうして生み出される構造と実装するシステムとの間で無理なくバランスし続けることがシステムを上手に運用するコツで,それこそが言語を選択する最重要ポイントだと思う(仕事ならね)。

公理によって与えられる暗黙の制約。この制約が集合の要素同士をしっかり結びつける。単純にしばるのではない、相互に秩序ある関係を結ぶ。言い換えれば――公理によって与えられる制約が構造を生み出しているのだ
数学ガール/フェルマーの最終定理より

システムを維持するために遺産や負債を抱え続けなければならない場合もあるが(それでも限度というか寿命はあるけど),そうでないならわざわざレガシーを選択する必然性は微塵もない。

ブックマーク


  1. Haskell では関数の引数は1つしかとれないためカリー化は必須の要件となる。意図的にカリー化を避けたいのであれば add (x, y) = x + y のように引数を組(tuple)にすればよい。 ↩︎

  2. 部分適用を上手く使えば,いわゆる OAOO (Once And Only Once) 原則に基づいて効率的なコードにできる。念のために言うと,部分適用を構成するのにカリー化は必要条件ではない。 ↩︎