カリー化と部分適用の違いは何ですか?
-
03-07-2019 - |
質問
インターネットでは、他の人のカレーの例はカレーではなく、実際には部分的な適用であるというさまざまな苦情をよく目にします。
部分的なアプリケーションとは何か、それがカリー化とどのように異なるのかについて、きちんとした説明はありません。一般的な混乱があるようで、同等の例は、ある場所ではカリー化、別の場所では部分的な適用として説明されています。
誰かが両方の用語の定義とそれらの違いの詳細を教えてもらえますか?
解決
Curryingは、 n 引数の単一の関数を、それぞれ単一の引数を持つ n 関数に変換します。次の関数がある場合:
function f(x,y,z) { z(x(y));}
カレーの場合、次のようになります。
function f(x) { lambda(y) { lambda(z) { z(x(y)); } } }
f(x、y、z)の完全なアプリケーションを取得するには、これを行う必要があります:
f(x)(y)(z);
多くの関数型言語では、 f x y z
を記述できます。 fxy
または f(x)(y)のみを呼び出すと、部分的に適用された関数を取得します。—戻り値は lambda( z){z(x(y))}
xおよびyの値を f(x、y)
に渡します。
部分アプリケーションを使用する1つの方法は、 fold のように、関数を一般化された関数の部分アプリケーションとして定義することです。
function fold(combineFunction, accumulator, list) {/* ... */}
function sum = curry(fold)(lambda(accum,e){e+accum}))(0);
function length = curry(fold)(lambda(accum,_){1+accum})(empty-list);
function reverse = curry(fold)(lambda(accum,e){concat(e,accum)})(empty-list);
/* ... */
@list = [1, 2, 3, 4]
sum(list) //returns 10
@f = fold(lambda(accum,e){e+accum}) //f = lambda(accumulator,list) {/*...*/}
f(0,list) //returns 10
@g = f(0) //same as sum
g(list) //returns 10
他のヒント
それらの違いを確認する最も簡単な方法は、実際の例を検討することです。入力として2つの数値を受け取り、出力として数値を返す関数 Add
があると仮定します。 Add(7、5)
は 12
を返します。この場合:
-
7
の値を持つAdd
関数を部分的に適用すると、出力として新しい関数が提供されます。その関数自体は1つの数値を入力として受け取り、数値を出力します。そのため:Partial(Add, 7); // returns a function f2 as output // f2 takes 1 number as input and returns a number as output
これを行うことができます:
f2 = Partial(Add, 7); f2(5); // returns 12; // f2(7)(5) is just a syntactic shortcut
-
Currying 関数
Add
は、出力として新しい関数を提供します。その関数自体は1つの数値を入力として受け取り、別の新しい関数をまだ出力します。この3番目の関数は、入力として1つの数値を受け取り、出力として数値を返します。そのため:Curry(Add); // returns a function f2 as output // f2 takes 1 number as input and returns a function f3 as output // i.e. f2(number) = f3 // f3 takes 1 number as input and returns a number as output // i.e. f3(number) = number
これを行うことができます:
f2 = Curry(Add); f3 = f2(7); f3(5); // returns 12
つまり、「カレー」および「部分適用」 2つのまったく異なる機能です。 Curryingの入力は1つだけですが、部分的なアプリケーションの入力は2つ(またはそれ以上)です。
どちらも関数を出力として返しますが、返される関数は上記のようにまったく異なる形式です。
注:これは、 F#の基本から取得したもので、.NET開発者向けの優れた入門記事です。関数型プログラミングに。
Curryingは、多くの引数を持つ関数をシリーズに分割することを意味します それぞれが1つの引数を取り、最終的に生成する関数の 元の関数と同じ結果。カレーはおそらく最も 特に関数型プログラミングを初めて使用する開発者にとって難しいトピック 多くの場合、部分的なアプリケーションと混同されます。職場で両方を見ることができます この例では:
let multiply x y = x * y let double = multiply 2 let ten = double 5
すぐに、ほとんどとは異なる動作が表示されます 命令型言語。 2番目のステートメントは、新しい関数を作成します 2つの引数を取る関数に1つの引数を渡すことにより、doubleと呼ばれます。 結果は、1つのint引数を受け入れて、 xが2、yに等しい乗算を呼び出した場合と同じ出力 その引数に等しい。動作に関しては、これと同じです コード:
let double2 z = multiply 2 z
多くの場合、人々は誤って乗算をカリー化して二重にすると言います。 しかし、これはいくぶん真実です。乗算関数はカリー化されていますが、 F#の関数は次のようにカリー化されているため、定義されたときに発生します。 デフォルト。 double関数が作成されると、より正確になります 乗算関数が部分的に適用されていると言います。
乗算関数は、実際には一連の2つの関数です。最初 関数は1つのint引数を取り、別の関数を返します。 xを特定の値に効果的にバインドします。この関数も受け入れます yにバインドする値と考えることができるint引数。後 この2番目の関数を呼び出すと、xとyの両方がバインドされるため、結果は doubleの本体で定義されているxとyの積。
doubleを作成するには、乗算チェーンの最初の関数 関数は、乗算を部分的に適用するために評価されます。結果として 関数にはdoubleという名前が付けられます。 doubleが評価されるとき、それは使用します その引数と部分的に適用された値を作成する 結果。
興味深い質問。少し検索した後、"部分関数アプリケーションはカリー化されていません" が最良の説明を提供しました見つけた。 実用的の違いは私には特に明白だとは言えませんが、私はFPの専門家ではありません...
別の便利なページ(まだ完全には読んでいませんが)は、" Java Closuresを使用した課金および部分アプリケーション" 。
これは広く混同されている用語のペアのように見えますが、気を付けてください。
別のスレッド https://stackoverflow.com/a/12846865/1685865 でこれに回答しました。つまり、部分関数アプリケーションは、特定の多変数関数のいくつかの引数を修正して引数の少ない別の関数を生成することです。一方、カリー化は、N個の引数の関数を単項関数を返す単項関数に変換することです... [カリー化はこの投稿の最後に示されています。]
Curryingは理論的に興味深いものです。単項関数のみを使用して計算を表現できます(つまり、 every 関数は単項です)。実際には、副産物として、言語に機能がカリー化されている場合、多くの有用な(ただしすべてではない)部分的な機能アプリケーションを簡単にすることができる手法です。繰り返しますが、部分的なアプリケーションを実装する唯一の手段ではありません。そのため、部分的な適用が他の方法で行われるシナリオに遭遇する可能性がありますが、人々はそれをカリー化と誤解しています。
(カレーの例)
実際には、書くだけではありません
lambda x: lambda y: lambda z: x + y + z
または同等のjavascript
function (x) { return function (y){ return function (z){ return x + y + z }}}
の代わりに
lambda x, y, z: x + y + z
カリー化のため。
Curryingは、関数 f
を取り、新しい関数 h
を返す one 引数の関数です。 h
は X
から引数を取り、 Y
を Z にマップする function を返すことに注意してください。 code>:
curry(f) = h
f: (X x Y) -> Z
h: X -> (Y -> Z)
部分アプリケーションは、 2つ(またはそれ以上)引数の関数であり、関数 f
および f
への1つ以上の追加引数を取ります。新しい関数 g
:
part(f, 2) = g
f: (X x Y) -> Z
g: Y -> Z
2つの引数を持つ関数では次の等式が成り立つため、混乱が生じます。
partial(f, a) = curry(f)(a)
両側で同じ1引数関数が生成されます。
この場合、カリー化は1つの引数の関数を返しますが、部分的なアプリケーションは複数の引数の関数を返します。
違いは動作にもありますが、カリー化は元の関数全体を再帰的に(引数ごとに1回)変換しますが、部分適用は1ステップの置換にすぎません。
出典:ウィキペディアカレー。
カレーと部分的なアプリケーションの違いは、次のJavaScriptの例を使用して最もわかりやすく説明できます。
function f(x, y, z) {
return x + y + z;
}
var partial = f.bind(null, 1);
6 === partial(2, 3);
部分適用では、アリティが小さい関数になります。上記の例では、 f
のアリティは3ですが、 partial
のアリティは2のみです。さらに重要なことは、部分的に適用された関数はすぐに結果を返します呼び出し時に、カリー化チェーンの別の機能ではありません。したがって、 partial(2)(3)
のようなものが表示されている場合、実際には部分的なアプリケーションではありません。
さらに読む:
部分的なアプリケーションは、使用される引数が結果の関数に完全に統合される新しい関数を作成する必要があります。
ほとんどの関数型言語は、クロージャを返すことでカリー化を実装しています。部分的に適用された場合、ラムダの下で評価しないでください。そのため、部分適用を面白くするには、カリー化と部分適用を区別し、部分適用をカリー化とラムダでの評価と見なす必要があります。
私はここで非常に間違っている可能性があります。理論的な数学や関数型プログラミングの強力なバックグラウンドがないためです一方、部分的なアプリケーション(実際)は、引数の数が不定の可変個性関数でより適切に機能します。以前の回答の例のいくつかはこの説明に反していますが、概念を分けるのに最も役立ちました。この例を検討してください(簡潔さのためにCoffeeScriptで記述されています。さらに混乱する場合はおmyび申し上げますが、必要に応じて説明を求めてください):
# partial application
partial_apply = (func) ->
args = [].slice.call arguments, 1
-> func.apply null, args.concat [].slice.call arguments
sum_variadic = -> [].reduce.call arguments, (acc, num) -> acc + num
add_to_7_and_5 = partial_apply sum_variadic, 7, 5
add_to_7_and_5 10 # returns 22
add_to_7_and_5 10, 11, 12 # returns 45
# currying
curry = (func) ->
num_args = func.length
helper = (prev) ->
->
args = prev.concat [].slice.call arguments
return if args.length < num_args then helper args else func.apply null, args
helper []
sum_of_three = (x, y, z) -> x + y + z
curried_sum_of_three = curry sum_of_three
curried_sum_of_three 4 # returns a function expecting more arguments
curried_sum_of_three(4)(5) # still returns a function expecting more arguments
curried_sum_of_three(4)(5)(6) # returns 15
curried_sum_of_three 4, 5, 6 # returns 15
これは明らかに不自然な例ですが、任意の数の引数を受け入れる関数を部分的に適用すると、いくつかの予備データを使用して関数を実行できることに注意してください。関数のカリー化は似ていますが、すべてのNパラメーターが考慮されるまでのみ、Nパラメーター関数を分割して実行できます。
繰り返しますが、これは私が読んだものからの私の見解です。誰もが同意しない場合は、即座に下票するのではなく、理由についてコメントをいただければ幸いです。また、CoffeeScriptが読みにくい場合は、coffeescript.orgにアクセスして、「try coffeescript」をクリックしてください。そして、コードを貼り付けて、コンパイルされたバージョンを確認します。ありがとう!
私は学習中にこの質問をたくさんしましたが、それ以来何度も質問されてきました。違いを説明する最も簡単な方法は、両方とも同じであるということです:)説明させてください...明らかに違いがあります。
部分的な適用とカリー化の両方には、おそらく一度にすべてではない関数への引数の提供が含まれます。かなり標準的な例は、2つの数値を追加することです。擬似コード(実際にはキーワードのないJS)では、基本関数は次のようになります。
add = (x, y) => x + y
&quot; addOne&quot;が必要な場合関数、私はそれを部分的に適用するか、カレーすることができます:
addOneC = curry(add, 1)
addOneP = partial(add, 1)
これらを使用するのは明らかです:
addOneC(2) #=> 3
addOneP(2) #=> 3
では、違いは何ですか?さて、それは微妙ですが、部分的なアプリケーションにはいくつかの引数を指定する必要があり、返される関数は次の呼び出しでメイン関数を実行します一方、カリーは必要な引数がすべて揃うまで待機し続けます:
curriedAdd = curry(add) # notice, no args are provided
addOne = curriedAdd(1) # returns a function that can be used to provide the last argument
addOne(2) #=> returns 3, as we want
partialAdd = partial(add) # no args provided, but this still returns a function
addOne = partialAdd(1) # oops! can only use a partially applied function once, so now we're trying to add one to an undefined value (no second argument), and we get an error
要するに、部分的なアプリケーションを使用していくつかの値を事前に入力します。次にメソッドを呼び出すと実行され、すべての未定義の引数が未定義のままになることを知っています。関数シグネチャを実現するために必要な回数だけ部分的に適用された関数を継続的に返す場合は、カリー化を使用します。最後の不自然な例:
curriedAdd = curry(add)
curriedAdd()()()()()(1)(2) # ugly and dumb, but it works
partialAdd = partial(add)
partialAdd()()()()()(1)(2) # second invocation of those 7 calls fires it off with undefined parameters
これがお役に立てば幸いです!
UPDATE:一部の言語またはlib実装では、アリティ(最終評価の引数の合計数)を部分的なアプリケーション実装に渡すことができます。これにより、2つの説明が混乱して混乱します。 2つの手法はほぼ互換性があります。
簡単な回答
カレー:を使用すると、関数を呼び出して、複数の呼び出しに分割し、呼び出しごとに1つの引数を指定できます。
部分:関数を呼び出し、複数の呼び出しに分割し、呼び出しごとに複数の引数を提供できます。
シンプルなヒント
どちらも、少ない引数を提供する関数を呼び出すことができます(または、より良いのは、引数を累積的に提供すること)。実際、両方とも関数の特定の引数に特定の値を(呼び出しごとに)バインドします。
実際の違いは、関数に3つ以上の引数がある場合に確認できます。
単純なe(c)(サンプル)
(Javascriptで)
function process(context, success_callback, error_callback, subject) {...}
コンテキストやコールバックなど、常に同じ引数を渡すのはなぜですか?関数のいくつかの値をバインドするだけ
processSubject = _.partial(process, my_context, my_success, my_error)
subject1 および foobar で呼び出します
processSubject('subject1');
processSubject('foobar');
快適ですね。 &#128521;
currying では、1回につき1つの引数を渡す必要があります
curriedProcess = _.curry(process);
processWithBoundedContext = curriedProcess(my_context);
processWithCallbacks = processWithBoundedContext(my_success)(my_error); // note: these are two sequential calls
result1 = processWithCallbacks('subject1');
// same as: process(my_context, my_success, my_error, 'subject1');
result2 = processWithCallbacks('foobar');
// same as: process(my_context, my_success, my_error, 'foobar');
免責事項
すべての学術的/数学的な説明をスキップしました。原因はわかりません。たぶん助けた&#128579;
ここには他にも素晴らしい答えがありますが、Javaでのこの例(私の理解によると)は一部の人々にとって有益であると信じています:
public static <A,B,X> Function< B, X > partiallyApply( BiFunction< A, B, X > aBiFunction, A aValue ){
return b -> aBiFunction.apply( aValue, b );
}
public static <A,X> Supplier< X > partiallyApply( Function< A, X > aFunction, A aValue ){
return () -> aFunction.apply( aValue );
}
public static <A,B,X> Function< A, Function< B, X > > curry( BiFunction< A, B, X > bif ){
return a -> partiallyApply( bif, a );
}
カリー化により、引数を1つ使用して関数を作成できます。部分アプリケーションでは、1つ以上の引数をハードコーディングするラッパー関数を作成します。
コピーと貼り付けを行う場合、次の方がノイズが多くなりますが、タイプがより寛容になるため、扱いやすくなります。
public static <A,B,X> Function< ? super B, ? extends X > partiallyApply( final BiFunction< ? super A, ? super B, X > aBiFunction, final A aValue ){
return b -> aBiFunction.apply( aValue, b );
}
public static <A,X> Supplier< ? extends X > partiallyApply( final Function< ? super A, X > aFunction, final A aValue ){
return () -> aFunction.apply( aValue );
}
public static <A,B,X> Function< ? super A, Function< ? super B, ? extends X > > curry( final BiFunction< ? super A, ? super B, ? extends X > bif ){
return a -> partiallyApply( bif, a );
}
これを書いているとき、私はカレーとカレーを混同しました。それらは関数の逆変換です。変換とその逆が表すものを取得する限り、実際に何を呼ぶかは重要ではありません。
Uncurryingは非常に明確に定義されていません(むしろ、すべてのアイデアの精神を捉える「矛盾する」定義があります)。基本的に、複数の引数を取る関数を、単一の引数を取る関数に変えることを意味します。たとえば、
(+) :: Int -> Int -> Int
今、どのようにしてこれを単一の引数を取る関数に変えるのですか?もちろんカンニングです!
plus :: (Int, Int) -> Int
plusが単一の引数(2つのもので構成される)を受け取ることに注意してください。スーパー!
これのポイントは何ですか? 2つの引数を取る関数があり、引数のペアがある場合、その関数を引数に適用しても期待どおりの結果が得られることを知っておくと便利です。実際、それを行うための配管はすでに存在しているため、明示的なパターンマッチングなどを行う必要はありません。あなたがしなければならないことは次のとおりです:
(uncurry (+)) (1,2)
では、部分関数アプリケーションとは何ですか? 2つの引数を持つ関数を1つの引数を持つ関数に変換する別の方法です。ただし、動作は異なります。繰り返しますが、例として(+)を取り上げましょう。単一のIntを引数として取る関数にどのように変換できますか?チート!
((+) 0) :: Int -> Int
これは、任意のIntにゼロを追加する関数です。
((+) 1) :: Int -> Int
任意のIntに1を追加します。など。これらの各ケースでは、(+)が「部分的に適用」されています。
この質問をするほとんどの人はすでに基本的な概念に精通しているので、それについて話す必要はないと思います。混乱する部分はオーバーラップです。
概念を完全に使用できるかもしれませんが、これらの概念を一緒に理解すると、この疑似原子的なアモルファス概念のぼかしになります。欠けているのは、それらの境界がどこにあるかを知ることです。
それぞれが何であるかを定義する代わりに、違いと境界だけを強調する方が簡単です。
Currying は、関数を定義するときです。
部分アプリケーションは、関数を呼び出しするときです。
アプリケーションは、関数を呼び出すための数学的な話です。
Partial アプリケーションでは、カリー化された関数を呼び出して、戻り値の型として関数を取得する必要があります。