“部分適用 (partial application)” と “カリー化 (currying)” について

Pocket

Haskell などの (純粋遅延) 関数型言語を学習する際の理解しにくい概念に「部分適用」と「カリー化」があります。実は両者とも非常に単純で、理解してしまえば難しくはないのですが、最初に学習する時には以下の疑問が沸き立ちます。

  • 「部分適用」「カリー化」のメリットって何だろう?
  • 「部分適用」「カリー化」の存在意義がわからない

誤解を恐れずに書くと、「部分適用」や「カリー化」って無くても困らないんですよね。

ただ、「部分適用」や「カリー化」があるとプログラミングが非常に楽しくなります。また、実用性の観点から見てもこれは非常に役立ちますし、「部分適用」と「カリー化」が理解できてこそ、初めて関数型プログラマのスタートラインに立ったと言っても過言ではありません。

特に Haskell では、パズルのような感覚でプログラミングすることが比較的多くありますが、これは「部分適用」によって得られていると思っています。さらに、この「部分適用」を最大限活かすために「カリー化」が使われます。

既に色々なところでカリー化と部分適用について語られていますが、この記事によって一人でも新たに理解できたらなら、意味があるかなぁと思います。

 

対象としている人

  • 「部分適用」と「カリー化」について自分の理解を確認したい
  • Haskell を少し知っている
  • 関数型言語を少し学んだ
  • プログラミング Haskell の説明では、理解できなかった等

 

部分適用とは

「部分適用」は英語では「partial application」と呼ばれています。

まず、「適用 (application)」「適用する (apply)」とは以下のような操作です。

これは Haskell の関数 (function) では ($) として定義されています。

具体的な使い方は上記の通りです。1ステップずつ簡約 (reduction) してみます。

このようにして neg3 1 の値が -1 となることがわかります。

ここで ($) の型を見たとき、一見すると ($) は引数を1つしか取れない関数しか引数に取らないように見えるかもしれません。

(というか、初学者のほとんどはそう見えていると思います。僕もそうでした)

 

しかし、少し見方を変えると次のように考えることができます。

先ほどと同じ ($) ですが、型変数 (type variable) b を1引数の関数と考えることで、2引数関数の適用が可能であることがわかります。

同様の考え方で任意長の引数の関数が適用可能であることも理解できるでしょう。(3引数であれば、上記の型変数 c を (d -> e) として考える)

当然ながら、定義は先ほどと同じです。定義が変わるということは異なる関数であることを意味します。最終的に「1つの定義から「部分適用」によって、とても有益な関数をいくつも作れる。」という事実を示したいのです。

ちなみに、ここまでの話において「部分適用」は一度も出現していません。ここまでで、「適用」がどのような操作になるのかを見てきました。

 

では、ここからは「部分適用」がどのような操作になるかを見ていきましょう。

上記の例は ($) に引数を1つずつ適用した場合の型を表しています。

「部分適用」とは引数を適用し、関数を返す操作と考えることができます。

この関数を返すという部分が「部分適用」の本質だと感じます。

 

func を1ステップずつ簡約すると、部分適用になっている部分はλ式で表現されています。

ここが関数を返す部分です。

余談ではありますが、関数型言語に慣れるまでは、「関数を返す関数」や「再帰」といった概念が少し不思議に思えます。学習し始めた頃は関数を返すという意味が本当に理解できないものです。

以下の関数の型はどちらも同じで、括弧を明示的に表記しているかどうかの違いしかありません。

しかしながら、この関数を最初に理解したときは、多くの方が上の意味で理解したのではないでしょうか。つまり、[a] を何らかの関数によって [b] に変換するような操作が map であると。

しかし、不思議なことに関数型言語の理解が深まると下のような視点が広がります。これは、(a -> b) の関数を ([a] -> [b]) の関数に持ち上げることに他なりません。

つまり、見ている対象が違うのです。前者では「値」を中心に理解が進んでいますが、後者は「関数」を中心として理解しています。このような例は本当に多く出現します。以下にその一例を挙げます

「部分適用」によって、型が合うのであれば関数同士が合成可能となり、より抽象的な定義が可能となります。

1つ具体的な例を示します。

これは、常に第一引数のみを返す定数関数です。最初のうちはどこで使うのか悩みますが、結構頻繁に使います。では、ここで常に第二引数のみを返す関数を定義してみましょう。

直接定義するのは非常に簡単です。しかし、上記の定義は以下のように書き直すことが可能です。

const 関数に id 関数を部分適用することで、「常に第一引数のみを返す関数」が、「常に第二引数を返す関数」に変化しました。これって凄くないですか?

上記の const’ 関数は以下のように考えれば納得できます。

わざわざ新しい関数 const’ を定義しなくても、小さな関数 (id, const) を上手く組み合わせることで、欲しい関数が作れる。これがパズルのような感覚でプログラミングできる1つの理由でもあり、「部分適用」のメリットになります。

もう一つ、実用的な部分適用の例を挙げます。タプルのリストの第一 (または、第二) 要素でソートする例です。

部分適用を使えばこんな感じで定義できちゃいます。抽象的に定義されているおかげで一度理解すれば、ソースコードをそのまま「第一要素 (fst) で (on) 比較する (compare) ことによってソートせよ (sortBy)」と何となく読む事ができます。

さらには、fst, snd と適用する関数を変えることによって、比較する要素を変更可能です。これはまさに部分適用の恩恵によるものです。

 

カリー化とは?

「カリー化 (currying)」とは、論理学者の Haskell Curry にちなんで名付けられました。これは非常に単純であり、以下の操作のことです。

こんな事して何が嬉しいのか?と言いますと、Haskell では「全ての関数がカリー化」されていますが、これは「部分適用」と非常に相性が良いのです。多引数の関数を1引数の関数 (関数を返す) に変換すれば、「部分適用」できるチャンスが増えますよね。

 

まとめ

以上が、「部分適用」と「カリー化」について僕が言いたいことの全てです。よくある教え方として

  1. 「カリー化」について説明する
  2. 「部分適用」について説明する
  3. 部分適用によって (+1) のような便利なインクリメント関数が作れます

という流れがあります。確か、プログラミングHaskellもこの流れだったと思いますが、この説明の仕方には以下の問題があると個人的には感じています。

  • カリー化から教えてしまうと、カリー化の目的が明確にならず、何のために存在しているのか理解できない
  • インクリメント関数が作れたところで部分適用のメリットが伝わらない

部分適用を使いこなせると、かっこいい関数型プログラマになれると思います!

 

内容について、間違い等ある場合は指摘してもらえると助かります。

“部分適用 (partial application)” と “カリー化 (currying)” について

コメントを残す

メールアドレスが公開されることはありません。

Top