高階関数を理解する
Haskell は関数型言語として、高階関数 (HOF) をサポートしています。数学では、HOF は汎関数と呼ばれますが、それらを理解するために数学は必要ありません。Java のような通常の命令型プログラミングでは、関数は整数や文字列などの値を受け取り、それらに対して何らかの処理を行い、他の型の値を返すことができます。
しかし、関数自体が値と変わらず、関数を引数として受け入れたり、別の関数から返すことができるとしたら? f a b c = a + b - c
は退屈な関数で、合計a
しb
てから減算しc
ます。しかし、この関数を一般化できれば、もっと興味深い関数になる可能性a
がb
あります。c
それとも引く代わりに割りますか?
は、数値を返す 2 つの数値の関数にすぎないことを覚えておいてください。(+)
特別なことは何もないので、数値を返す 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つの引数の無名関数です。基本的に、呼び出すときは引数が 1 つの関数を作成します。これは、受け取ったものに 2 を追加するだけです。したがって、12 に等しく、25 に等しくなります。m
f
m
n
g (+) 2
let f = g (+) 2 in f 10
let f = g (*) 5 in f 5
( Scheme を例として使用したHOF の説明も参照してください。)
カリー化を理解する
カリー化とは、複数の引数の関数を、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]
。この新しい関数はさらに別のリストを受け入れることができ、最終的に type の新しいリストを返し[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 つだけ適用すると、 type の関数が返されるため、これは理にかなっています[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)
。(/)
しかし、の代わりに除算演算子が必要な場合はどうすればよいでしょうreplicate
か? 中置関数の場合、Haskell では、いずれかの引数を使用して部分的に適用できます。
これはセクションと呼ばれます:(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/)]
would yield [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: difference between をお読みください。Learn You a Haskell の(ドット) と $ (ドル記号)とパーツの違いを理解してください。