カレーのことを知ったばかりで、コンセプトは理解できたとは思いますが、カレーを使うメリットはあまりありません。
簡単な例として、2つの値を追加する関数(MLで記述)を使用します。カレーなしのバージョンは
fun add(x, y) = x + y
と呼ばれます
add(3, 5)
カレーバージョンは
fun add x y = x + y
(* short for val add = fn x => fn y=> x + y *)
と呼ばれます
add 3 5
関数の定義と呼び出しから1組の括弧を削除するのは、単なる構文上の砂糖のようです。関数型言語の重要な機能の1つとして挙げられているカレーを見てきたのですが、今のところ少し困惑しています。タプルを取る関数の代わりに、それぞれ単一のパラメーターを使用する関数のチェーンを作成するという概念は、構文の単純な変更に使用するのはかなり複雑に見えます。
少し単純な構文がカレーの唯一の動機ですか、それとも非常に単純な例では明らかではない他のいくつかの利点を見逃していますか?カレーは構文糖だけですか?
カリー化された関数を使用すると、専門化するため、より抽象的な関数を簡単に再利用できます。あなたが追加機能を持っているとしましょう
add x y = x + y
また、リストのすべてのメンバーに2を追加したいとします。 Haskellではこれを行うでしょう:
map (add 2) [1, 2, 3] -- gives [3, 4, 5]
-- actually one could just do: map (2+) [1, 2, 3], but that may be Haskell specific
ここでの構文は、関数を作成する必要がある場合よりも軽くなりますadd2
add2 y = add 2 y
map add2 [1, 2, 3]
または、匿名のラムダ関数を作成する必要がある場合:
map (\y -> 2 + y) [1, 2, 3]
また、異なる実装から抽象化することもできます。 2つのルックアップ関数があるとしましょう。次のように、キーと値のペアのリストとキーから値へのマップ、およびキーから値とキーから値へのマップからの1つ。
lookup1 :: [(Key, Value)] -> Key -> Value -- or perhaps it should be Maybe Value
lookup2 :: Map Key Value -> Key -> Value
次に、キーから値へのルックアップ関数を受け入れる関数を作成できます。上記のルックアップ関数のいずれかを渡して、リストまたはマップでそれぞれ部分的に適用することができます。
myFunc :: (Key -> Value) -> .....
結論として、カリー化は適切です。軽量化構文を使用して関数を特殊化/部分的に適用し、これらの部分的に適用された関数をmap
やfilter
などの高次関数に渡すことができるためです。高次関数(関数をパラメーターとして使用するか、結果として生成する)は関数型プログラミングの基本であり、カリー化関数と部分的に適用される関数を使用すると、高次関数をより効果的かつ簡潔に使用できます。
実際の答えは、カリー化することで匿名関数の作成がずっと簡単になるということです。最小限のラムダ構文があっても、それは勝利のようなものです。比較:
_map (add 1) [1..10]
map (\ x -> add 1 x) [1..10]
_
醜いラムダ構文がある場合、それはさらに悪いことです。 (私はあなた、JavaScript、Scheme、Pythonを見ています。)
より高次の関数を使用するにつれて、これはますます便利になります。 Haskellでは他の言語よりもmore高次関数を使用していますが、実際にはラムダ構文lessを使用していることがわかりました。ラムダは、部分的に適用される関数です。 (他のほとんどの場合、それを名前付き関数に抽出します。)
より基本的には、関数のどのバージョンが「正規」であるかが常に明らかであるとは限りません。たとえば、map
を取ります。 map
のタイプは、次の2つの方法で記述できます。
_map :: (a -> b) -> [a] -> [b]
map :: (a -> b) -> ([a] -> [b])
_
「正しい」ものはどれですか?言うのは難しいです。実際には、ほとんどの言語は最初の言語を使用します。マップは関数とリストを受け取り、リストを返します。ただし、基本的に、mapが実際に行うのは、通常の関数を関数のリストにマップすることです。つまり、関数を受け取って関数を返します。マップがカレー化されている場合、この質問に答える必要はありません。非常にエレガントな方法でbothを実行します。
これは、map
をリスト以外の型に一般化すると、特に重要になります。
また、カレーは本当にそれほど複雑ではありません。これは実際、ほとんどの言語で使用されているモデルを少し単純化したものです。言語に組み込まれた複数の引数の関数の概念は必要ありません。これは、基礎となるラムダ計算をより厳密に反映しています。
もちろん、MLスタイルの言語には、カレー形式または非カリー形式の複数の引数の概念はありません。 f(a, b, c)
構文は実際にはタプル_(a, b, c)
_をf
に渡すことに対応しているため、f
は引数のみを受け取ります。これは実際、次のようなものを書くのが非常に自然になるので、他の言語が持つことを望む非常に有用な区別です:
_map f [(1,2,3), (4,5,6), (7, 8, 9)]
_
これは、複数の引数が組み込まれている言語では簡単にはできません。
最初のクラスのオブジェクトとして渡す関数があり、それを評価するために必要なすべてのパラメーターをコード内の1つの場所で受け取っていない場合、カリー化が役立つことがあります。取得した1つ以上のパラメーターを適用し、その結果を、パラメーターの多い別のコードに渡して、そこで評価を完了するだけです。
これを実現するためのコードは、最初にすべてのパラメーターをまとめる必要がある場合よりも単純になります。
また、単一のパラメーターを取る関数(別のカリー化された関数)がすべてのパラメーターと明確に一致する必要がないため、コードが再利用される可能性が高くなります。
(私はHaskellで例を挙げます。)
関数型言語を使用する場合、関数を部分的に適用できると非常に便利です。 Haskellの_(== x)
_のように、引数が指定された項True
と等しい場合にx
を返す関数です。
_mem :: Eq a => a -> [a] -> Bool
mem x lst = any (== x) lst
_
カレー化しないと、コードが多少読みにくくなります。
_mem x lst = any (\y -> y == x) lst
_
これは 暗黙のプログラミング に関連しています(Haskell wikiの Pointfreeスタイル も参照してください)。このスタイルは、変数によって表される値ではなく、関数の作成と、情報が関数のチェーンをどのように流れるかに焦点を当てています。例を変数をまったく使用しない形式に変換できます。
_mem = any . (==)
_
ここでは、_==
_をa
から_a -> Bool
_までの関数として、any
を_a -> Bool
_から_[a] -> Bool
_までの関数として表示します。単純にそれらを構成することにより、結果が得られます。これはすべてカレーのおかげです。
逆の非カレー化は、いくつかの状況でも役立ちます。たとえば、リストを2つの部分(10より小さい要素と残りの要素)に分割し、それら2つのリストを連結したいとします。リストの分割は partition
_(< 10)
_によって行われます(ここではカレー_<
_も使用しています)。結果は_([Int],[Int])
_型です。結果をその最初と2番目の部分に抽出し、_++
_を使用してそれらを結合する代わりに、_++
_をアンカレーすることでこれを直接行うことができます。
_uncurry (++) . partition (< 10)
_
実際、_(uncurry (++) . partition (< 10)) [4,12,11,1]
_は_[4,1,12,11]
_に評価されます。
また、重要な理論上の利点もあります。
(a, b) -> c
_からa -> (b -> c)
への変換は、後者の関数の結果が_b -> c
_型であることを意味します。つまり、結果は関数です。カレーの主な動機(少なくとも最初は)は実用的ではなく理論的でした。特に、カリー化を使用すると、実際にセマンティクスを定義したり、製品のセマンティクスを定義したりせずに、マルチ引数関数を効果的に取得できます。これは、別のより複雑な言語と同じくらい表現力のある単純な言語につながるため、望ましいです。
カレーは単なる構文上の砂糖ですが、砂糖の働きを少し誤解していると思います。あなたの例をとると、
fun add x y = x + y
は実際には構文上の砂糖です
fun add x = fn y => x + y
つまり、(add x)は引数yを取り、xをyに追加する関数を返します。
fun addTuple (x, y) = x + y
これはタプルを取り、その要素を追加する関数です。これら2つの関数は実際にはかなり異なります。彼らは異なる引数を取ります。
リストのすべての数値に2を追加する場合:
(* add 2 to all numbers using the uncurried function *)
map (fn x => addTuple (x, 2)) [1,2,3]
(* using the curried function *)
map (add 2) [1,2,3]
結果は[3,4,5]
。
一方、リスト内の各タプルを合計したい場合、addTuple関数は完全に適合します。
(* Sum each Tuple using the uncurried function *)
map addTuple [(10,2), (10,3), (10,4)]
(* sum each Tuple using curried function *)
map (fn (a,b) => add a b) [(10,2), (10,3), (10,4)]
結果は[12,13,14]
。
カリー化された関数は、マップ、フォールド、アプリ、フィルターなどの部分的なアプリケーションが役立つ場合に最適です。指定されたリストで最大の正の数を返すこの関数を考えてください。正の数がない場合は0です。
- val highestPositive = foldr Int.max 0;
val highestPositive = fn : int list -> int
私がまだ言及していないもう1つのことは、カリー化によってアリティよりも(限られた)抽象化が可能になることです。
Haskellのライブラリの一部であるこれらの関数を考えてください
(.) :: (b -> c) -> (a -> b) -> a -> c
either :: (a -> c) -> (b -> c) -> Either a b -> c
flip :: (a -> b -> c) -> b -> a -> c
on :: (b -> b -> c) -> (a -> b) -> a -> a -> c
どちらの場合も、型変数c
は関数型にすることができるため、これらの関数は引数のパラメーターリストのプレフィックスで機能します。カリー化しないと、関数アリティを抽象化するための特別な言語機能が必要になるか、さまざまなアリティに特化したこれらの関数のさまざまなバージョンが必要になります。
カレーは、単なる構文上の砂糖ではありません。
add1
(uncurried)とadd2
(curried)の型シグネチャについて考えてみましょう。
add1 : (int * int) -> int
add2 : int -> (int -> int)
(どちらの場合も、型シグニチャーの括弧はオプションですが、わかりやすくするために含めています。)
add1
は、int
とint
の2タプルを取り、int
を返す関数です。 add2
は、int
を取り、別の関数を返し、int
を受け取る関数です。 int
を返します。
関数の適用を明示的に指定すると、2つの本質的な違いがより明確になります。最初の引数を2番目の引数に適用する関数(カレーではない)を定義してみましょう。
apply(f, b) = f b
これで、add1
とadd2
の違いがよりはっきりとわかります。 add1
は2タプルで呼び出されます:
apply(add1, (3, 5))
しかし、add2
はint
で呼び出され、その戻り値は別のint
で呼び出されます。
apply(apply(add2, 3), 5)
編集:カレーの本質的な利点は、部分的なアプリケーションを無料で入手できることです。パラメータに5を追加したタイプint -> int
(たとえば、リストに対してmap
にする)の関数が必要だとします。 addFiveToParam x = x+5
と書くことも、インラインラムダで同等のことを行うこともできますが、はるかに簡単に(特に、これよりも簡単な場合)add2 5
!と書くこともできます。
私の限られた理解はそのようなものです:
1)部分関数アプリケーション
部分関数アプリケーションは、引数の数が少ない関数を返すプロセスです。 3つのうち2つの引数を指定すると、3-2 = 1の引数を取る関数が返されます。 3つのうち1つの引数を指定すると、3-1 = 2つの引数を取る関数が返されます。必要に応じて、3つの引数のうち3つを部分的に適用することもでき、引数を取らない関数を返します。
したがって、次の関数があるとします。
_f(x,y,z) = x + y + z;
_
1をxにバインドし、それを上記の関数f(x,y,z)
に部分的に適用すると、次のようになります。
_f(1,y,z) = f'(y,z);
_
ここで:f'(y,z) = 1 + y + z;
ここで、yを2に、zを3にバインドし、部分的にf'(y,z)
を適用すると、次のようになります。
_f'(2,3) = f''();
_
ここで:f''() = 1 + 2 + 3
;
これで、いつでも、f
、_f'
_、または_f''
_の評価を選択できます。だから私はできる:
_print(f''()) // and it would return 6;
_
または
_print(f'(1,1)) // and it would return 3;
_
2)カレー
一方、Curryingは、関数を1つの引数関数のネストされたチェーンに分割するプロセスです。複数の引数を指定することはできません。1または0です。
したがって、同じ関数が与えられます:
_f(x,y,z) = x + y + z;
_
カレー化すると、3つの関数のチェーンが得られます。
_f'(x) -> f''(y) -> f'''(z)
_
どこ:
_f'(x) = x + f''(y);
f''(y) = y + f'''(z);
f'''(z) = z;
_
次に、_x = 1
_を指定してf'(x)
を呼び出すと、
_f'(1) = 1 + f''(y);
_
新しい関数が返されます。
_g(y) = 1 + f''(y);
_
_y = 2
_でg(y)
を呼び出す場合:
_g(2) = 1 + 2 + f'''(z);
_
新しい関数が返されます。
_h(z) = 1 + 2 + f'''(z);
_
最後に、h(z)
を_z = 3
_で呼び出す場合:
_h(3) = 1 + 2 + 3;
_
_6
_が返されます。
)閉鎖
最後に、Closureは、関数とデータを1つの単位として一緒にキャプチャするプロセスです。関数クロージャーは、0から無数の引数を取ることができますが、渡されないデータも認識します。
繰り返しますが、同じ関数が与えられます:
_f(x,y,z) = x + y + z;
_
代わりにクロージャーを書くことができます:
_f(x) = x + f'(y, z);
_
どこ:
_f'(y,z) = x + y + z;
_
_f'
_はx
に閉鎖されています。つまり、_f'
_はf
内にあるxの値を読み取ることができます。
したがって、_x = 1
_を指定してf
を呼び出すと、次のようになります。
_f(1) = 1 + f'(y, z);
_
あなたは閉鎖を得るでしょう:
_closureOfF(y, z) =
var x = 1;
f'(y, z);
_
ここで、closureOfF
を_y = 2
_および_z = 3
_で呼び出した場合:
_closureOfF(2, 3) =
var x = 1;
x + 2 + 3;
_
これは_6
_を返します
結論
カリー化、部分的なアプリケーション、およびクロージャーは、関数をより多くの部分に分解するという点で、すべて多少似ています。
カリー化は、複数の引数の関数を、単一の引数の関数を返す単一の引数のネストされた関数に分解します。 1つまたはそれ以下の引数の関数をカリー化しても意味がありません。
部分的なアプリケーションは、複数の引数の関数を、現在欠落している引数が指定された値に置き換えられたより小さな引数の関数に分解します。
クロージャは関数を関数とデータセットに分解します。渡されなかった関数内の変数は、データセット内を調べて、評価を求められたときにバインドする値を見つけることができます。
これらすべてについて混乱しているのは、それぞれが他のサブセットを実装するために使用できることです。したがって、本質的には、これらはすべて実装の詳細の一部です。すべての値を事前に収集する必要がなく、関数の一部を目立たない単位に分解しているため、関数の一部を再利用できるという点で、これらはすべて同様の値を提供します。
開示
私は決してそのトピックの専門家ではありません。これらについて学び始めたばかりなので、現在の理解を提供しますが、指摘しておくべき間違いがある可能性があります。発見した。
カリー化(部分的なアプリケーション)では、いくつかのパラメーターを修正することにより、既存の関数から新しい関数を作成できます。これは、匿名関数がキャプチャされた引数を別の関数に渡す単純なラッパーである字句閉鎖の特殊なケースです。字句の閉包を作成するための一般的な構文を使用してこれを行うこともできますが、部分的なアプリケーションは単純化された構文糖を提供します。
これが、LISPプログラマーが関数型のスタイルで作業するときに 部分的なアプリケーションのライブラリ を使用する理由です。
引数に3を追加する関数を提供する(lambda (x) (+ 3 x))
の代わりに、(op + 3)
のようなものを書くことができます。したがって、いくつかのリストのすべての要素に3を追加するには、(mapcar (op + 3) some-list)
ではなく(mapcar (lambda (x) (+ 3 x)) some-list)
を使用します。このop
マクロは、いくつかの引数x y z ...
を取り、(+ a x y z ...)
を呼び出す関数を作成します。
多くの純粋に関数型の言語では、部分的なアプリケーションが構文に組み込まれているため、op
演算子はありません。部分的なアプリケーションをトリガーするには、必要な数よりも少ない引数で関数を呼び出すだけです。 "insufficient number of arguments"
エラーを生成する代わりに、結果は残りの引数の関数になります。
機能について
fun add(x, y) = x + y
f': 'a * 'b -> 'c
という形式です。
評価するには
add(3, 5)
val it = 8 : int
カレー機能について
fun add x y = x + y
評価するには
add 3
val it = fn : int -> int
部分的な計算、具体的には(3 + y)の場合、次のようにして計算を完了することができます。
it 5
val it = 8 : int
2番目のケースのaddは、f: 'a -> 'b -> 'c
の形式です。
ここでカレーが行っているのは、2つの合意を取る関数を、1つの結果のみを返すものに変換することです。 部分評価
なぜこれが必要なのでしょうか?
RHSのx
は、単なる通常のintではなく、補完のために2秒かかるため、完了するまでに時間がかかる複雑な計算です。
x = twoSecondsComputation(z)
したがって、関数は次のようになります
fun add (z:int) (y:int) : int =
let
val x = twoSecondsComputation(z)
in
x + y
end;
タイプadd : int * int -> int
ここで、ある範囲の数値に対してこの関数を計算したいので、マッピングしてみましょう
val result1 = map (fn x => add (20, x)) [3, 5, 7];
上記の場合、twoSecondsComputation
の結果は毎回評価されます。つまり、この計算には6秒かかります。
ステージングとカレーの組み合わせを使用すると、これを回避できます。
fun add (z:int) : int -> int =
let
val x = twoSecondsComputation(z)
in
(fn y => x + y)
end;
カレー形のadd : int -> int -> int
今、できる
val add' = add 20;
val result2 = map add' [3, 5, 7, 11, 13];
twoSecondsComputation
は一度だけ評価する必要があります。スケールを上げるには、2秒を15分または任意の時間に置き換え、100個の数値に対するマップを作成します。
概要:部分評価のツールとしてより高いレベルの関数を他の方法で使用すると、カリー化が最適になります。その目的を実際に単独で示すことはできません。
カリー化は柔軟な機能構成を可能にします。
「カレー」という機能を作りました。この文脈では、どのようなロガーを取得するか、それがどこから来るかは気にしません。アクションが何であるか、それがどこから来るのかは気にしません。私が気にしているのは、入力を処理することだけです。
var builder = curry(function(input, logger, action) {
logger.log("Starting action");
try {
action(input);
logger.log("Success!");
}
catch (err) {
logger.logerror("Boo we failed..", err);
}
});
var x = "My input.";
goGatherArgs(builder)(x); // Supplies action first, then logger somewhere.
ビルダー変数は、自分の作業を行う入力を受け取る関数を返す関数を返す関数です。これは単純な便利な例であり、目に見えるオブジェクトではありません。
関数のすべての引数がない場合は、カリー化が有利です。関数を完全に評価している場合、大きな違いはありません。
カリー化することで、まだ必要ではないパラメーターについて言及することを回避できます。より簡潔で、スコープ内の別の変数と衝突しないパラメーター名を見つける必要がありません(これが私のお気に入りの利点です)。
たとえば、関数を引数として取る関数を使用する場合、「3を入力に追加」や「入力を変数vと比較」などの関数が必要な場合がよくあります。カリー化により、これらの関数は簡単に記述できます:add 3
および(== v)
。カレー化せずに、ラムダ式を使用する必要があります:x => add 3 x
およびx => x == v
。ラムダ式は2倍の長さで、x
がすでにスコープ内にある場合は、x
以外の名前の選択に関連する少量の忙しい作業があります。
カリー化に基づく言語の副次的な利点は、関数の汎用コードを作成するときに、パラメーターの数に基づいて何百ものバリアントが作成されないことです。たとえば、C#では、「カレー」メソッドには、Func <R>、Func <A、R>、Func <A1、A2、R>、Func <A1、A2、A3、R>などのバリアントが必要です。永遠に。 Haskellでは、Func <A1、A2、R>に相当するものは、Func <Tuple <A1、A2>、R>またはFunc <A1、Func <A2、R >>(およびFunc <R>に似ています)はFunc <Unit、R>)に似ているため、すべてのバリアントは単一のFunc <A、R>ケースに対応します。
私が考えることができる主な推論(そして私は決してこの主題の専門家ではありません)は、関数が些細なものから非自明なものに移るにつれて、その利点を示し始めます。この性質のほとんどの概念を備えたすべての些細なケースでは、実際のメリットはありません。ただし、ほとんどの関数型言語は処理操作でスタックを多用します。この例として PostScript または [〜#〜] lisp [〜#〜] を検討してください。カリー化を利用することで、関数をより効果的に積み重ねることができます。この利点は、操作がますます重要にならないほど大きくなります。カリー化された方法では、コマンドと引数をスタックに順番にスローし、必要に応じてポップして、適切な順序で実行できます。
カリー化は、関数を返す能力に決定的に(決定的にさえ)依存します。
この(考案された)疑似コードを考えてみましょう。
var f =(m、x、b)=> ...何かを返す...
3つ未満の引数でfを呼び出すと、関数が返されると規定しましょう。
var g = f(0、1); //これは、0と1にバインドされた関数(mおよびx)を返し、もう1つの引数(b)を受け入れます。
var y = g(42); // mとxに0と1を使用して、不足している3番目の引数を指定してgを呼び出します
引数を部分的に適用して、再利用可能な関数(ユーザーが指定した引数にバインドされている)を取得できることは、非常に便利です(およびDRY)。