私は自分自身にHaskellを教えている最中であり、次のタイプの署名について疑問に思っていました。
Prelude> :t ($)
($) :: (a -> b) -> a -> b
Prelude>
それをどのように解釈すればよいですか(しゃれは意図されていません)?
半類似の結果も不可解であることが証明されています。
Prelude> :t map
map :: (a -> b) -> [a] -> [b]
Prelude>
map
から始めます。 map
関数は、リスト内のすべての要素に操作を適用します。もしわたしが持っていたら
add3 :: Int -> Int
add3 x = x + 3
次に、Int
を使用してこれをmap
sのリスト全体に適用できます。
> map add3 [1, 2, 3, 4]
[4, 5, 6, 7]
したがって、型の署名を見ると
map :: (a -> b) -> [a] -> [b]
最初の引数は(a -> b)
であることがわかります。これは、a
を受け取り、b
を返す関数です。 2番目の引数は[a]
であり、これはタイプa
の値のリストであり、戻り値のタイプ[b]
は、タイプb
の値のリストです。したがって、平易な英語では、map
関数は値のリスト内の各要素に関数を適用し、それらの値をリストとして返します。
これがmap
を高階関数にするものであり、引数として関数を取り、それを処理します。 map
を確認する別の方法は、型シグネチャに括弧を追加してそれを作成することです。
map :: (a -> b) -> ([a] -> [b])
したがって、関数をa
からb
から[a]
から[b]
への関数に変換する関数と考えることもできます。
関数($)
のタイプは
($) :: (a -> b) -> a -> b
そして、のように使用されます
> add3 $ 1 + 1
5
right(この場合は1 + 1
)にあるものを取得し、それをleft(ここではadd3
)の関数に渡すだけです。何でこれが大切ですか?便利なfixity、つまり演算子の優先順位があり、
> add3 (1 + 1)
したがって、右側にあるものはすべて、左側に渡される前に基本的に括弧で囲まれます。これにより、複数の関数をチェーン化するのに役立ちます。
> add3 $ add3 $ add3 $ add3 $ 1 + 1
より良いです
> add3 (add3 (add3 (add3 (1 + 1))))
かっこを閉じる必要がないためです。
さて、すでに述べたように、$
は、カリー化を忘れて、たとえばC++のように見れば簡単に理解できます。
template<typename A, typename B>
B dollar(std::function<B(A)> f, A x) {
return f(x);
}
しかし実際には、これには関数を値に適用するだけではありません。 $
とmap
の署名の間の明らかな類似性は、実際にはかなり深い圏論的意味を持っています。どちらも、ファンクターの射作用の例です。
私たちがいつも扱っているカテゴリーHaskでは、オブジェクトは型です。 ( それは少し混乱します 、しかし心配しないでください)。射は関数です。
最もよく知られている(endo-)functorsは、 同名の型クラス のインスタンスを持つものです。しかし実際には、数学的には、ファンクターはオブジェクトをオブジェクトに、射を射にマップするものにすぎません。1。 map
(しゃれを意図したものだと思います!)は例です。オブジェクト(つまり、型)A
を取り、それを型[A]
にマップします。そして、任意の2つのタイプA
とB
の場合、射(つまり関数)A -> B
を取り、タイプ[A] -> [B]
の対応するリスト関数にマップします。 。
これは、ファンクタークラスの署名操作の特殊なケースです。
fmap :: Functor f => (a->b) -> (f a->f b)
ただし、数学ではこのfmap
に名前を付ける必要はありません。したがって、identity functorも存在する可能性があります。これは、単に任意のタイプをそれ自体に割り当てます。そして、それ自体へのすべての射:
($) :: (a->b) -> (a->b)
「アイデンティティ」は明らかにより一般的に存在し、任意のタイプの値をそれ自体にマップすることもできます。
id :: a -> a
id x = x
そして確かに、可能な実装は
($) = id
1オブジェクトと射をマッピングするanythingはファンクターです... ファンクターの法則 を満たす必要があります。
($)
は単なる関数適用です。型a->b
の関数、型a
の引数を取得し、関数を適用して、型b
の値を返します。
map
は、関数型のシグネチャを読み取ることがそれを理解するのにどのように役立つかを示すすばらしい例です。 map
の最初の引数はa
を取り、b
を返す関数であり、2番目の引数はタイプ[a]
のリストです。したがって、map
はタイプa->b
の関数をa
値のリストに適用します。そして、結果の型は確かに型[b]
-b
値のリストです!
(a->b)->[a]->[b]
は、「関数とリストを受け入れて別のリストを返す」、および「タイプa->b
の関数を受け入れてタイプ[a]->[b]
の別の関数を返す」と解釈できます。このように見ると、map
"upgrade" f(このコンテキストでは「lift」という用語がよく使用されます)はリストで機能します。double
が整数を2倍にする関数の場合、次にmap double
は、リスト内のすべての整数を2倍にする関数です。