カリー化が提供できる大きな利点が見当たらないので、カリー化についてよく理解していないと思います。おそらく、誰かがなぜそれがとても便利なのかを示す例で私を啓発することができます. それは本当に利点とアプリケーションを持っていますか、それとも単に過大評価された概念ですか?
7 に答える
(カリー化と部分適用にはわずかな違いがありますが、密接に関連しています。それらはしばしば混合されるため、両方の用語を扱います。)
私が最初に利点に気付いたのは、スライスされた演算子を見たときでした。
incElems = map (+1)
--non-curried equivalent: incElems = (\elems -> map (\i -> (+) 1 i) elems)
IMO、これは完全に読みやすいです。ここで、タイプ(+)
が(Int,Int) -> Int
*である場合、これはカレーなしのバージョンであり、(直感に反して)エラーが発生しますが、カレーは期待どおりに機能し、タイプは[Int] -> [Int]
です。
コメントでC#ラムダについて言及されました。incElems
C#では、関数を指定すると、次のように記述できますplus
。
var incElems = xs => xs.Select(x => plus(1,x))
x
ポイントフリースタイルに慣れている場合は、ここが冗長であることがわかります。論理的には、そのコードは次のように減らすことができます
var incElems = xs => xs.Select(curry(plus)(1))
これは、C#ラムダを使用した自動部分適用がないためにひどいものです。そしてそれは、カリー化が実際にどこで役立つかを決定するための重要なポイントです。ほとんどの場合、それが暗黙的に発生する場合です。私にとってmap (+1)
は、最も読みやすく、次に来るので、本当に正当な理由がない場合は.Select(x => plus(1,x))
、のバージョンはおそらく避ける必要があります。curry
さて、読みやすい場合、利点は、より短く、より読みやすく、雑然としたコードになります-ポイントフリースタイルの乱用がない限り、それはそれで行われます(私は大好き(.).(.)
ですが、それは...特別です)
また、ラムダ計算は、1つの値(ただし高階)関数しかないため、カレー関数を使用しないと不可能になります。
*もちろん実際にはNum
ですが、今のところこのように読みやすくなっています。
更新:カリー化が実際にどのように機能するか。
plus
C#のタイプを見てください:
int plus(int a, int b) {..}
C#の用語ではなく、数学的に話された値のタプルを指定する必要があります。2番目の値を除外することはできません。Haskellの用語では、それは
plus :: (Int,Int) -> Int,
次のように使用できます
incElem = map (\x -> plus (1, x)) -- equal to .Select (x => plus (1, x))
文字が多すぎて入力できません。将来、これをもっと頻繁に行いたいとしましょう。ここに少しヘルパーがあります:
curry f = \x -> (\y -> f (x,y))
plus' = curry plus
これは
incElem = map (plus' 1)
これを具体的な値に当てはめてみましょう。
incElem [1]
= (map (plus' 1)) [1]
= [plus' 1 1]
= [(curry plus) 1 1]
= [(\x -> (\y -> plus (x,y))) 1 1]
= [plus (1,1)]
= [2]
ここであなたは仕事で見ることができますcurry
。これは、標準のhaskellスタイルの関数アプリケーション(plus' 1 1
)を「tupled」関数の呼び出しに変換します。または、より高いレベルで表示すると、「tupled」を「untupled」バージョンに変換します。
幸いなことに、自動部分適用があるので、ほとんどの場合、これについて心配する必要はありません。
スライスパン以来最高のものではありませんが、とにかくラムダを使用している場合は、ラムダ構文を使用せずに高階関数を使用する方が簡単です. 比較:
map (max 4) [0,6,9,3] --[4,6,9,4]
map (\i -> max 4 i) [0,6,9,3] --[4,6,9,4]
関数型プログラミングを使用している場合、これらの種類の構成要素は十分に頻繁に使用されます。これは便利な近道であり、問題を少し高いレベルから考えることができます。max 4
ランダム関数ではなく、" " 関数に対してマッピングしています。として定義されている関数(\i -> max 4 i)
。これにより、より高いレベルの間接的な思考をより簡単に開始できます。
let numOr4 = map $ max 4
let numOr4' = (\xs -> map (\i -> max 4 i) xs)
numOr4 [0,6,9,3] --ends up being [4,6,9,4] either way;
--which do you think is easier to understand?
とはいえ、万能薬ではありません。関数のパラメーターは、カリー化でやろうとしていることに対して間違った順序になることがあるため、とにかくラムダに頼る必要があります。しかし、このスタイルに慣れると、それとうまく機能するように機能を設計する方法を学び始め、それらのニューロンが脳内で接続し始めると、以前は複雑だった構造が比較して明白に見えるようになります.
カリー化の利点の 1 つは、特別な構文や演算子を必要とせずに関数を部分的に適用できることです。簡単な例:
mapLength = map length
mapLength ["ab", "cde", "f"]
>>> [2, 3, 1]
mapLength ["x", "yz", "www"]
>>> [1, 2, 3]
map :: (a -> b) -> [a] -> [b]
length :: [a] -> Int
mapLength :: [[a]] -> [Int]
この関数は、カリー化のためにmap
型を持つと見なすことができるため、 を最初の引数として適用すると、 type の関数が生成されます。(a -> b) -> ([a] -> [b])
length
mapLength
[[a]] -> [Int]
カリー化には他の回答で言及されている便利な機能がありますが、多くの場合、言語に関する推論を単純化したり、他の方法よりもはるかに簡単にコードを実装したりするのにも役立ちます。たとえば、カリー化とは、すべての関数が と互換性のある型を持つことを意味しますa ->b
。型が を含むコードを書くa -> b
と、引数の数に関係なく、そのコードを任意の関数で動作させることができます。
これの最もよく知られた例はApplicative
クラスです:
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
そして使用例:
-- All possible products of numbers taken from [1..5] and [1..10]
example = pure (*) <*> [1..5] <*> [1..10]
このコンテキストでは、 type の関数を typeのリストで動作するようpure
に<*>
適応させます。部分的な適用のため、これは、タイプの関数をand 、または、andなどで動作するように適合させることもできることを意味します。a -> b
[a]
a -> b -> c
[a]
[b]
a -> b -> c -> d
[a]
[b]
[c]
これが機能する理由a -> b -> c
は、 と同じだからですa -> (b -> c)
:
(+) :: Num a => a -> a -> a
pure (+) :: (Applicative f, Num a) => f (a -> a -> a)
[1..5], [1..10] :: Num a => [a]
pure (+) <*> [1..5] :: Num a => [a -> a]
pure (+) <*> [1..5] <*> [1..10] :: Num a => [a]
カリー化のもう 1 つの別の用途は、Haskell では型コンストラクターを部分的に適用できることです。たとえば、このタイプがある場合:
data Foo a b = Foo a b
...実際には、多くのコンテキストで記述することは理にかなっていますFoo a
。たとえば、次のようになります。
instance Functor (Foo a) where
fmap f (Foo a b) = Foo a (f b)
つまり、Foo
kind を持つ 2 つのパラメーターの型コンストラクター* -> * -> *
です。Foo a
を 1 つの型だけに部分的に適用しFoo
た は、 kind を持つ型コンストラクタ* -> *
です。 Functor
kind の型コンストラクタに対してのみインスタンス化できる型クラスです* -> *
。Foo a
はこのようなものなので、Functor
インスタンスを作成できます。
あなたが質問している文脈を特定せずにカリー化の利点が何であるかを尋ねることは幾分疑わしいです:
- 関数型言語のように、カリー化は、よりローカルな変更が加えられたものと見なされる場合があります。この場合、明示的なタプリングされたドメインに置き換えることができます。しかし、これはカリー化がこれらの言語で役に立たないということではありません。ある意味で、カレー関数を使用したプログラミングでは、高階関数を扱っている状況に直面することが多いため、より機能的なスタイルでプログラミングしているように「感じる」ことができます。確かに、ほとんどの場合、関数のすべての引数を「入力」しますが、関数を部分的に適用された形式で使用する場合は、カレー形式で行う方が少し簡単です。
curry
また、関数型プログラミング言語内の特定の利便性にuncurry
も役立ちます。Haskell内の矢印は、使用する場所の具体例であり、矢印のさまざまな部分などに適用するためのビットcurry
と考えることができます。uncurry
- 場合によっては、機能的なプログラム以上のものを考えたい場合は、カリー化/カリー化を、建設的論理の排除と導入のルールを述べる方法として提示できます。これは、それが存在する理由のよりエレガントな動機への接続を提供します。
- 場合によっては、たとえばCoqでは、カレー関数とタプル関数を使用すると、アプリケーションに応じて、操作が簡単または困難になるさまざまな誘導スキームが生成される可能性があります。
私は以前、カリー化はタイピングの手間を省く単純な構文糖衣だと思っていました。たとえば、書く代わりに
(\ x -> x + 1)
私はただ書くことができます
(+1)
後者はすぐに読みやすくなり、起動するための入力が少なくなります。
便利なショートカットだとしたら、なぜ大騒ぎするのでしょうか。
関数型はカリー化されているため、関数が持つ引数の数が多様なコードを記述できることがわかりました。
たとえば、このQuickCheck
フレームワークでは、ランダムに生成されたテスト データを関数に与えることで、関数をテストできます。入力タイプが自動生成できるすべての関数で機能します。しかし、カリー化のおかげで、作成者はそれをリグすることができたので、これは任意の数の引数で機能します。関数がカリー化されていない場合、引数の数ごとに異なるテスト関数が存在することになり、それは面倒なことになります。