Hatena::ブログ(Diary)

あどけない話

2011-09-06

カリー化談義

最近、スタートHaskellで「カリー化された関数のメリットは何か?」という質問が出た。そのすぐ後に、kmizuさんがカリー化の誤用に対して警鐘を鳴らしてしていた。僕からするとkmizuさんの「カリー化の定義」も誤用に思えたので、調べるとともに考えたことのまとめ。

いろんな定義

「カリー化する」という用語は、すくなくとも以下の3つの意味で使われているようだ。

  1. 部分適用という意味
    • これは明らかに間違い
  2. 「複数の引数を取る関数」を「一引数を取る関数のチェインに直す」こと
    • これはkmizuさんの定義。世間でもよく使われる。
  3. 「構造体を一つ取る関数」を「構造体のメンバーを複数の引数にばらし、一引数を取る関数のチェインに直す」こと

「部分適用」の意味で使うのは明らかに間違いのなで排除。定義2と3について議論する。あとで、部分適用とは何かに戻る。

カリー化されている

「カリー化する」と言った場合、定義2と3では意味が明らかに違う。すなわち、入力が「複数の引数を取る関数」なのか、「構造体を一つ取る関数」なのかという点が異なる。

しかし、「カリー化されて」いると言った場合、同じ意味を持つ。すなわち、出力された関数は、「一引数を取る関数のチェイン」の形をしている。

言葉で説明すると分かりにくいので、JavaScript を使って例を示す。足し算の + は二項演算子だけれど、二引数関数だとも考えられる。これを、「一引数を取る関数のチェイン」の形にしてみよう。

var plus = 
function (x) {
    return function (y) {
	return x + y;
    }
}

これは以下のように使う。

(plus(1))(2); → 3

チェインの意味が分かりましたか?

「カリー化されて」いるは、安心して使っていいようだ。

定義2のカリー化する

上記が、まさに定義2のカリー化の例だ。+ という「二引数関数」を plus という「一引数を取る関数のチェイン」に直したのだから。

定義3のカリー化する

この定義では、対象となる関数引数として一つの構造体を取る。ここでは、構造体を配列で代用しよう。以下のような関数を考える。

function plusArray(ar) {
    return ar[0] + ar[1];
}

もちろん、以下のように動作する。

plusArray([1,2]); → 3

Haskell には curry という関数があって、これは定義3のカリー化をする。JavaScript で実装するなら、こんな感じになる。

function curry(f) {
    return function (x) {
	return function (y) {
	    return f([x,y]);
	}
    }
}

使ってみよう。

var curryPlusArray = curry(plusArray); 
(curryPlusArray(1))(2); → 3

どっちが正しい?

僕は「定義2も誤用なんじゃない?」と思っていたので、wikipediaCurrying を読み直してみた。すると、定義2も定義3の両方の意味があると書いてあった。

In mathematics and computer science, currying is the technique of transforming a function that takes multiple arguments (or an n-tuple of arguments) in such a way that it can be called as a chain of functions each with a single argument (partial application). I

tuple というのは、無名構造体の意味。

という訳で、「カリー化する」という場合は、文脈によって定義2なのか定義3なのかを判断しないといけないというのが正しいらしい。

多くのプログラミング言語では、関数は「カリー化されて」いない。このような言語の話をしている場合に「カリー化する」と言った場合は、定義2であると解釈すべき。

Haskell では、すべての関数は「カリー化されて」いる。つまり、引数は一つだけで、値か関数を返す。このような世界で「カリー化する」と言った場合は、定義3であると解釈すべき。

カリー化の利点

「定義は分かったけど、結局カリー化すると何が嬉しいの?」と思う人もいるだろう。カリー化のメリットは、部分適用である。ある関数を雛形として、引数をカスタマイズした関数を作り出せる。以下に例を示す。

var plus1 = plus 1;
plus1(2); → 3
var plus2 = plus 2;
plus2(2); → 4

高階関数と組み合わせると、その表記の簡潔さは一目瞭然となる。たとえば、map という高階関数があるとする。

function map(f, ar) {
    var ret = [];
    for(var i=0; i<ar.length; i++) {
	ret[i] = f(ar[i]);
    }
    return ret;
}

以下は、map に plus(1) という部分適用した関数を渡す例。

map(plus(1), [1,2,3]); →[2,3,4]

plus1 という関数を無駄に定義しなくて済んだ。

ちなみに Haskell では、二項演算子も map もカリー化されているので、以下のような記述も可能である。

map1 = map (+1)

Haskellの部分適用は的を射てない

複数の引数を取る関数に対して、いくつかの引数を固定できるなら、これは部分適用と言える。Scala にはこの機能があるそうだ。

しかし、カリー化された関数は、一引数しか取らない。部分なんてない。だから、カリー化された関数に部分適用するというのは、的を射てない表現である。しかし、Haskell の初心者には、こういう風に説明するの分かりやすいらしいので、この表現がまかり通っている。

kmizushimakmizushima 2011/09/07 21:04 こんばんは。水島です。カリー化談義、楽しく読ませていただきました。
ただ、些細な点ですが、2点ほど気になる部分があってのでコメントさせて
いただければ、と思います。

> という訳で、「カリー化する」という場合は、文脈によって定義2なのか定義3なのかを判断しないといけないというのが正しいらしい。

おっしゃる通り、厳密に言えば、定義2と定義3は異なりますが、
「プログラミング言語においては」、両者を厳密に区別する必要がある
のだろうか、というのが正直な気持ちです。というのは、Haskellのように
関数が常に一つの引数しか取らない言語において「カリー化する」
と言った場合、必然的に定義3を採用することになりますから、定義2を
考える意味がそもそもありません。一方で、「複数引数」を採用している
多くの言語では、やはり必然的に定義2を採用することになるでしょう。
もちろん、複数引数の言語であえて定義3のカリー化を考えることは可能
ですが、そのようなカリー化をあえて行う必然性がありませんし、その
ような使い方を見たこともほとんどありません。

というわけで、カリー化と部分適用の違いほど、両者の違いは
(プログラミング言語においては)大きく無いのでは、と私は思います。

> しかし、カリー化された関数は、一引数しか取らない。部分なんてない。だから、カリー化された関数に部分適用するというのは、本当は誤りである。

これには異議があります。

関数型プログラミングの文脈で「部分適用」と言った場合、初心者向けかどうかに関わらず、関数 f

f :: 'a1 -> 'a2 -> .. -> 'an

に対して、n個未満の引数を適用する事を指して、「部分適用」と呼ぶ用法
が元からあります。たとえば、Haskell 98 Language Reportの「3.5 Sections」において、

> Sections are a convenient syntax for partial application of binary operators.

という用法が見られます。ここで、「部分適用」とされているものは、2
項演算子

op :: 'a -> 'b -> 'c

に対して、(op e)のようにする事を指していますから、これは、山本さんの
定義に従えば誤用になります。しかし、Haskell 98 Language Reportは
「初心者向け」の説明文書ではなく、「言語仕様」に近い文書ですから、
そのような箇所で初歩的な用語の誤用をしたとは考えづらいです。

また、Standard MLなどに関する学術的な論文でも、カリー化された
関数に対して「部分適用」という用語を使っている例は、いくつも
見受けられます。

というわけで、カリー化された関数に部分適用するというのは誤用、
というのは誤りなのではないでしょうか。

以上、些細な点ですが、ツッコミでした。

kazu-yamamotokazu-yamamoto 2011/09/08 12:13 一つ目に関して:僕は以前定義2をわざわざ「カリー化の概念を用いて関数を定義する」と表現して区別していました。今回の議論で、話の内容に応じて理解すればいいと抽象化できた訳です。どなたかが「直積をどう表現しているかだけの違い」とつぶやいていましたが、まさにそうだと思います。抽象の壁を突破するには、いろいろ具体例を積み上げるしかないので、僕としては自分の理解過程は、まぁ妥当だったんじゃないかと思っています。

二つ目について:「誤用」は強い表現であり、間違って理解されるかもしれませんので、「的を射てない表現」に変えます。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/kazu-yamamoto/20110906/1315279311