私は Haskell と FP 全般について非常に初心者です。カリー化とは何かを説明した文章をたくさん読みましたが、実際にどのように動作するのかという説明は見つかりませんでした。
ここに関数があります:(+) :: a -> (a -> a)
とすると(+) 4 7
、関数は を受け取ってを受け取って を4
返す関数を返します。しかし、 はどうなるでしょうか? 最初の関数は をどのように処理するでしょうか? はをどのように処理するでしょうか?7
11
4
4
(a -> a)
7
より複雑な関数について考えると、事態はさらに混乱します。
max' :: Int -> (Int -> Int)
max' m n | m > n = m
| otherwise = n
(Int -> Int)
はパラメータを何と比較しますか? パラメータは 1 つしか取りませんが、 を実行するには 2 つ必要ですm > n
。
ベストアンサー1
高階関数を理解する
Haskellは関数型言語として高階関数(HOF)をサポートしています。数学ではHOFは関数ただし、これらを理解するために数学は必要ありません。Java などの通常の命令型プログラミングでは、関数は整数や文字列などの値を受け入れ、それらに対して何らかの処理を行い、別の型の値を返します。
しかし、関数自体が値と変わらず、関数を引数として受け取ったり、別の関数から関数を返したりできるとしたらどうでしょうか? はf a b c = a + b - c
退屈な関数でa
、b
と を足して を引きますc
。しかし、関数はもっと面白くなるかもしれません。一般化するa
では、とを足し算したいときもあればb
、掛け算したいときもある場合はどうすればよいでしょうか。あるいは、c
引き算ではなく割り算をしたいとしたらどうでしょうか。
は、(+)
単に 2 つの数値から数値を返す関数であり、特別なものではありません。したがって、 の代わりに、数値から数値を返す任意の関数を使用できます。 などと書くg a b c = a * b - c
だけh a b c = a + b / c
では不十分です。一般的なソリューションが必要です。結局のところ、私たちはプログラマーなのですから! Haskell では次のように実行されます。
let f g h a b c = a `g` b `h` c in f (*) (/) 2 3 4 -- returns 1.5
また、関数を返すこともできます。以下では、関数と引数を受け入れて、パラメータを受け入れて結果を返す別の関数を返す関数を作成します。
let g f n = (\m -> m `f` n); f = g (+) 2 in f 10 -- returns 12
構造(\m -> m `f` n)
とは匿名関数1 つの引数がと にm
適用されます。基本的に、 を呼び出すと、受け取った値に 2 を加算する 1 つの引数の関数が作成されます。したがって、 は12 に等しく、 は25 に等しくなります。f
m
n
g (+) 2
let f = g (+) 2 in f 10
let f = g (*) 5 in f 5
(参照HOFについての私の説明(Scheme を例に使用します。)
カレーを理解する
カレー作りは、複数の引数を取る関数を、1 つの引数を取る関数を返す 1 つの引数を取る関数に変換し、その関数が 1 つの引数を取る関数を返す、という処理を、値を返すまで繰り返していく手法です。思ったより簡単です。たとえば、次のような 2 つの引数を取る関数があるとします(+)
。
では、引数を 1 つだけ渡して関数を返すとしたらどうでしょう。この関数を後で使用して、この新しい関数に含まれる最初の引数を他のものに追加することができます。例:
f n = (\m -> n - m)
g = f 10
g 8 -- would return 2
g 4 -- would return 6
なんと、Haskell はデフォルトですべての関数をカリー化します。技術的に言えば、Haskell には複数の引数を取る関数はなく、引数が 1 つの関数のみがあり、その一部は引数が 1 つの新しい関数を返す場合があります。
型から明らかです。:t (++)
インタープリタで を記述します。(++)
は 2 つの文字列を連結する関数で、 を返します(++) :: [a] -> [a] -> [a]
。 型は ではなく です[a],[a] -> [a]
。つまり[a] -> [a] -> [a]
、 は(++)
1 つのリストを受け入れて 型の関数を返します[a] -> [a]
。 この新しい関数はさらに別のリストを受け入れることができ、最終的に 型の新しいリストを返します[a]
。
そのため、Haskell の関数適用構文には括弧やカンマがありません。Haskell の をf a b c
Python や Java の と比較してみてくださいf(a, b, c)
。これは奇妙な美的判断ではなく、Haskell では関数適用は左から右に進むため、f a b c
実際には であり(((f a) b) c)
、 がデフォルトでカリー化されることが分かれば完全に理にかなっていますf
。
ただし、型では、関連付けは右から左に行われるため、 は[a] -> [a] -> [a]
と同等です[a] -> ([a] -> [a])
。これらは Haskell では同じものであり、Haskell はこれらをまったく同じように扱います。これは理にかなっています。なぜなら、引数を 1 つだけ適用すると、型 の関数が返されるからです[a] -> [a]
。
一方、map
: は(a -> b) -> [a] -> [b]
、最初の引数として関数を受け取るため、括弧が付いています。
カリー化の概念を本当に理解するには、インタープリターで次の式の型を見つけてみてください。
(+)
(+) 2
(+) 2 3
map
map (\x -> head x)
map (\x -> head x) ["conscience", "do", "cost"]
map head
map head ["conscience", "do", "cost"]
部分的な申請とセクション
HOFとカリー化を理解したら、Haskellにはコードを短くするための構文がいくつかあります。1つまたは複数の引数を持つ関数を呼び出して、引数を受け入れる関数を返す場合、それは次のように呼ばれます。部分適用。
匿名関数を作成する代わりに、関数を部分的に適用することができることはすでにご存じのとおりです。そのため、 と書く代わりに、(\x -> replicate 3 x)
と書くことができます。しかし、の代わりに(replicate 3)
除算演算子を使用したい場合はどうでしょうか? 中置関数の場合、Haskell では、引数のいずれかを使用して部分的に適用することができます。(/)
replicate
これはセクション: は(2/)
と同等であり(\x -> 2 / x)
、 は(/2)
と同等です(\x -> x / 2)
。バックティックを使用すると、任意のバイナリ関数のセクションを取得できます。(2`elem`)
は と同等です(\xs -> 2 `elem` xs)
。
ただし、Haskell ではどの関数もデフォルトでカリー化されるため、常に 1 つの引数を受け入れるので、セクションは実際にはどの関数でも使用できます。たとえば、(+^)
4 つの引数を合計して 14 を返す奇妙な関数を考えてみましょうlet (+^) a b c d = a + b + c in (2+^) 3 4 5
。
作曲
簡潔で柔軟なコードを書くための他の便利なツールは構成そしてアプリケーションオペレータ. 合成演算子は(.)
関数を連結します。適用演算子は、($)
左側の関数を右側の引数に適用するだけなので、 とf $ x
同等ですf x
。ただし、 は($)
すべての演算子の中で最も優先順位が低いため、これを使用して括弧を取り除くことができます。 はf (g x y)
と同等ですf $ g x y
。
複数の関数を同じ引数に適用する必要がある場合にも役立ちます。map ($2) [(2+), (10-), (20/)]
は になります[4,8,10]
。(f . g . h) (x + y + z)
、f (g (h (x + y + z)))
、f $ g $ h $ x + y + z
はf . g . h $ x + y + z
同等ですが、(.)
と は($)
異なるものなので、Haskell: . (ドット) と $ (ドル記号) の違いそしてLearn You a Haskellからの一部違いを理解するために。