私は関数型プログラミングに関する記事を毎日読んでいて、可能な限りいくつかのプラクティスを適用しようとしています。しかし、私はカレーや部分的なアプリケーションで何がユニークなのか分かりません。
例として、このGroovyコードを見てみましょう。
def mul = { a, b -> a * b }
def tripler1 = mul.curry(3)
def tripler2 = { mul(3, it) }
tripler1
とtripler2
の違いがわかりません。どちらも同じではないですか? 「カレー化」は、Groovy、Scala、Haskellなどの純粋または部分的な関数型言語でサポートされています。ただし、別の名前付きまたは匿名を作成するだけで、同じこと(左カレー、右カレー、nカレーまたは部分的なアプリケーション)を実行できます。ほとんどの言語(Cでさえ)でパラメーターを元の関数(tripler2
など)に転送する関数またはクロージャー
ここで何か不足していますか? Grailsアプリケーションでカレーと部分的なアプリケーションを使用できる場所はありますが、「どう違うのですか」と自分に問いかけているので、ためらっています。
教えてください。
編集:皆さんは、デフォルトのパラメーターを元の関数に転送する別の関数を作成/呼び出すよりも、部分的なアプリケーション/カリー化が単に効率的であると言っていますか?
カリー化とは、n個の入力を受け取る関数を、それぞれが1個の入力を受け取るn個の関数に変換/表現することです。部分的な適用とは、関数への入力の一部を修正することです。
部分的に適用する動機は、主に、高次の関数ライブラリを作成しやすくすることです。たとえば、 C++ STL のアルゴリズムはすべて主に述語または単項関数を取ります。 bind1st を使用すると、ライブラリユーザーは値がバインドされた非単項関数をフックできます。したがって、ライブラリライターは、単項関数を使用してバイナリバージョンを提供するすべてのアルゴリズムに対して、オーバーロードされた関数を提供する必要はありません。
カレー自体は便利です。自由に好きな場所で部分的にアプリケーションを使用できるためです。つまり、部分的に適用するためにbind1st
のような関数が不要になります。
@jkとして。言及したように、カリー化はコードをより一般的にするのに役立ちます。
たとえば、次の3つの関数(Haskell)があるとします。
> let q a b = (2 + a) * b
> let r g = g 3
> let f a b = b (a 1)
ここでf
関数は2つの関数を引数として取り、1
を最初の関数に渡し、最初の呼び出しの結果を2番目の関数に渡します。
引数としてf
とq
を使用してr
を呼び出すと、効果的に次のようになります。
> r (q 1)
ここで、q
は1
に適用され、別の関数を返します(q
はカリー化されているため)。次に、この返された関数は、3
の引数が与えられる引数としてr
に渡されます。この結果は、9
の値になります。
ここで、他に2つの関数があるとします。
> let s a = 3 * a
> let t a = 4 + a
これらをf
に渡して、引数が7
または15
であるかどうかに応じて、s t
またはt s
の値を取得することもできます。これらの関数はどちらも関数ではなく値を返すため、f s t
またはf t s
で部分的なアプリケーションが実行されることはありません。
f
とq
を考慮してr
を記述した場合、部分的なアプリケーションの代わりにラムダ(無名関数)を使用した可能性があります。例:
> let f' a b = b (\x -> a 1 x)
しかし、これはf'
の一般性を制限していたでしょう。 f
は、引数q
とr
またはs
とt
で呼び出すことができますが、f'
はq
とr
でのみ呼び出すことができます-f' s t
とf' t s
はどちらもエラーになります。
[〜#〜]もっと[〜#〜]
f'
がq'
/r'
のペアで呼び出され、q'
が3つ以上の引数を取った場合でも、q'
は部分的にf'
に適用されます。
または、q
をf
の内側ではなく外側でラップすることもできますが、ネストされた厄介なラムダが残ります。
f (\x -> (\y -> q x y)) r
これは基本的に、最初にカリー化されたq
があったものです!
しかし、ほとんどの言語でパラメーターを元の関数(tripr2など)に転送する別の名前付き関数または匿名関数またはクロージャーを作成するだけで、同じこと(左カリー、右カリー、nカリー、または部分的なアプリケーション)を実行できます( Cさえも)
そしてオプティマイザはそれを見て、すぐに理解できるものに進みます。カリー化はエンドユーザーにとってはちょっとしたトリックですが、言語設計の観点からははるかに優れています。 reallyすべてのメソッドを単項として扱うのがいいA -> B
ここで、B
は別のメソッドである可能性があります。
これにより、高次関数を処理するために記述する必要のあるメソッドが簡素化されます。言語での静的分析と最適化には、既知の方法で動作する1つのパスしかありません。パラメータバインディングは、この一般的な動作を実行するためにフープを必要とするのではなく、設計から外れます。
部分適用には2つのポイントがあります。 1つ目は構文/利便性です。@ jkで述べたように、一部の定義は読み書きが簡単で短くなります。 (これがいかに素晴らしいかについては、 Pointfreeプログラミング をチェックしてください!)
2番目は、@ telastynが述べたように、関数のモデルに関するものであり、単に便利ではありません。 Haskellバージョンでは、部分的なアプリケーションで他の言語に慣れていないため、ここからサンプルを取得しますが、すべての関数は単一の引数を取ります。はい、機能は次のとおりです。
(:) :: a -> [a] -> [a]
単一の引数を取ります。関数型コンストラクタ->
の結合性のため、上記は次と同等です。
(:) :: a -> ([a] -> [a])
これは、a
を受け取り、関数[a] -> [a]
を返す関数です。
これにより、次のような関数を記述できます。
($) :: (a -> b) -> a -> b
any関数を適切な型の引数に適用できます。次のようなクレイジーなものでも:
f :: (t, t1) -> t -> t1 -> (t2 -> t3 -> (t, t1)) -> t2 -> t3 -> [(t, t1)]
f q r s t u v = q : (r, s) : [t u v]
f' :: () -> Char -> (t2 -> t3 -> ((), Char)) -> t2 -> t3 -> [((), Char)]
f' = f $ ((), 'a') -- <== works fine
さて、それは人為的な例でした。しかし、より便利なものには、このメソッドを含む Applicative type class が含まれます。
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
ご覧のとおり、$
ビットを削除した場合、タイプはApplicative f
と同じであり、実際、このクラスはコンテキストでの関数の適用を記述しています。したがって、通常の関数アプリケーションの代わりに:
ghci> map (+3) [1..5]
[4,5,6,7,8]
Applicativeコンテキストで関数を適用できます。たとえば、何かが存在するか欠落している可能性があるMaybeコンテキストでは、次のようになります。
ghci> Just map <*> Just (+3) <*> Just [1..5]
Just [4,5,6,7,8]
ghci> Just map <*> Nothing <*> Just [1..5]
Nothing
今本当にクールな部分は、Applicative型クラスが複数の引数の関数について何も言及していないことです-それにもかかわらず、それらを扱うことができます、f
のような6つの引数の関数でも:
fA' :: Maybe (() -> Char -> (t2 -> t3 -> ((), Char)) -> t2 -> t3 -> [((), Char)])
fA' = Just f <*> Just ((), 'a')
私の知る限り、一般的な形のApplicative型クラスは、部分的な適用の概念がないと不可能です。 (そこにいるすべてのプログラミングの専門家に-私が間違っているなら私を訂正してください!)もちろん、あなたの言語が部分的な適用に欠けているなら、あなたはできますビルドなんらかの形になっていますが、それは同じではありませんね。 :)