私は現在、型クラスとインスタンスに頭を悩ませようとしていますが、それらのポイントはまだよくわかりません。これまでのところ、この問題について2つの質問があります。
1)関数がその型クラスの関数を使用する場合、関数シグネチャに型クラスが必要なのはなぜですか。例:
f :: (Eq a) => a -> a -> Bool
f a b = a == b
署名に(Eq a)
を入れる理由。 ==
がa
に対して定義されていない場合、a == b
に遭遇したときにエラーをスローしないのはなぜですか?型クラスを先に宣言しなければならないことのポイントは何ですか?
2)型クラスと関数のオーバーロードはどのように関連していますか?
これを行うことはできません:
data A = A
data B = B
f :: A -> A
f a = a
f :: B -> B
f b = b
しかし、これを行うことは可能です:
data A = A
data B = B
class F a where
f :: a -> a
instance F A where
f a = a
instance F B where
f b = b
どうしたの?同じ名前で異なるタイプで動作する2つの関数を使用できないのはなぜですか... C++から来ているのは非常に奇妙だと思います。しかし、私はおそらくこれらのものが実際に何であるかについて間違った概念を持っています。しかし、これらの型クラスインスタンスでそれらをラップすると、できることです。
Haskellの学習と並行してこれらの主題について学習しているので、カテゴリを投げたり、理論的な単語を入力したりしてください。Haskellがここで物事を行う方法には理論的な根拠があると思います。
私は Willem Van Onsemの答え の多くに同意しますが、真にアドホックなオーバーロードに対する型クラスの主な利点の1つを見落としていると思います:抽象化。型クラスの代わりにアドホックオーバーロードを使用してMonad
操作を定義したと想像してください。
_-- Maybe
pure :: a -> Maybe a
pure = Just
(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
Just x >>= f = f x
Nothing >>= _ = Nothing
-- Either
pure :: a -> Either e a
pure = Right
(>>=) :: Either e a -> (a -> Either e b) -> Either e b
Right x >>= f = f x
Left err >>= _ = Left err
_
これで、すべてのモナドは上記のようにpure
と_>>=
_で表現できることがわかりましたが、alsoは、fmap
を使用して同等に表現できることを知っています。 、pure
、およびjoin
。したがって、anyモナドで機能するjoin
関数を実装できるはずです。
_join x = x >>= id
_
しかし、今は問題があります。 join
のタイプは何ですか?
明らかに、join
は設計上どのモナドでも機能するため、ポリモーフィックである必要があります。ただし、型シグネチャforall m a. m (m a) -> m a
を指定すると、明らかに間違っています。これは、すべての型で機能せずモナド型のみで機能するためです。したがって、型クラス制約が提供する操作_(>>=) :: m a -> (a -> m b) -> m b
_の存在の必要性を表す、型内の何かが必要です。
これを考えると、アドホックオーバーロードによって名前をオーバーロードできることが明らかになりますが、さまざまな実装が何らかの形で関連しているという保証がないため、オーバーロードされた名前を抽象化することはできません。could型クラスなしでモナドを定義しますが、join
、when
、unless
、mapM
、sequence
、およびその他すべての無料で入手できる素晴らしいものを定義できませんでした。 2つの操作のみを定義します。
したがって、Haskellでは、コードの再利用を可能にし、大量の重複を回避するために型クラスが必要です。しかし、bothtypeclass-styleoverloadingとtype-directed、ad-hoc nameoverloadingを使用できますか? はい、そして実際、イドリスはそうします。しかし、Idrisの型推論はHaskellの型推論とは大きく異なるため、Willemの回答の多くの理由から、Haskellよりもサポートする方が実現可能です。
要するに:それがHaskellが設計された方法だからです。
署名に
(Eq a)
を入れる理由。==
が定義されていない場合は、a == b
が発生したときにエラーをスローしないのはなぜですか?
なぜC++プログラムのシグネチャに型を入れるのですか(本文のアサーションとしてだけではありません)?それがC++の設計方法だからです。通常、どのプログラミング言語が構築されるかについての概念は、「明示的にする必要があるものを明示的にする」です。
Haskellモジュールがオープンソースであるとは言われていません。つまり、利用できるのは署名だけです。したがって、たとえば次のように書くと、次のようになります。
Prelude> foo A A
<interactive>:4:1: error:
• No instance for (Eq A) arising from a use of ‘foo’
• In the expression: foo A A
In an equation for ‘it’: it = foo A A
ここでは、foo
型クラスを持たない型を使用してEq
を頻繁に記述します。その結果、コンパイル時(またはHaskellが動的言語の場合は実行時)にのみ検出されるエラーが多数発生します。型シグニチャにEq a
を入れるという考え方は、事前にfoo
のシグニチャを調べて、型が型クラスのインスタンスであることを確認できるということです。
型シグニチャを自分で書く必要はないことに注意してください。Haskellは通常、関数のシグニチャを導出できますが、シグニチャには、関数を効果的に呼び出して使用するために必要なすべての情報を含める必要があります。タイプ制約を追加することで、開発をスピードアップします。
どうしたの?同じ名前で異なるタイプの2つの関数を使用できないのはなぜですか。
繰り返しますが、それがHaskellの設計方法です。関数型プログラミング言語の関数は「第一級市民」です。これは通常、これらに名前が付いていることを意味し、名前の衝突をできるだけ避けたいと考えています。 C++のクラスが通常一意の名前を持っているのと同じように(名前空間を除く)。
2つの異なる関数を定義するとします。
incr :: Int -> Int
incr = (+1)
incr :: Bool -> Bool
incr _ = True
bar = incr
次に、どのincr
をbar
で選択する必要がありますか?もちろん、型を明示的にすることもできます(つまり、incr :: Bool -> Bool
)が、多くのノイズが発生するため、通常はその作業を避けたいと考えています。
これを行わないもう1つの理由は、通常、型クラスは単なる関数のコレクションではないためです。これらの関数にcontractsを追加します。たとえば、Monad
型クラスは、関数間の特定の関係を満たす必要があります。たとえば、(>>= return)
はid
と同等である必要があります。言い換えれば、型クラス:
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m a
2つの独立した関数(>>=)
とreturn
については説明していません:これは関数のセットです。あなたはそれらの両方を持っているか(通常、特定の>>=
とreturn
の間のいくつかの契約がある)、またはこれらのどれもまったく持っていません。
これは質問1にのみ答えます(少なくとも直接)。
タイプシグニチャ_f :: a -> a -> Bool
_は_f :: forall a. a -> a -> Bool
_の省略形です。 f
は実際には機能しませんすべてのタイプa
は、_(==)
_が定義されているa
sに対してのみ機能します。 _(==)
_を持つ型に対するこの制限は、f :: forall a. (Eq a) => a -> a -> Bool
の制約_(Eq a)
_を使用して表されます。
「すべての人のために」/全称記号は、ハスケルの(パラメトリ)多態性の中核であり、とりわけ、 パラメトリシティ の強力で重要な特性を提供します。
Haskellは(とりわけ)2つの公理を保持しています:
あなたが持っていた場合
f :: A -> A
そして
f :: B -> B
その場合、Haskellで採用された原則によれば、f
はそれ自体で有効な式であり、それ自体はsingle型である必要があります。サブタイピングを使用してそれを行うことは可能ですが、型クラスのソリューションよりもはるかに複雑であると見なされていました。
同様に、Eq a
の必要性
(==) :: Eq a => a -> a -> Bool
==
のタイプは、それを使って何ができるかを完全に記述しなければならないという事実から来ています。特定のタイプでのみ呼び出すことができる場合、タイプシグネチャはそれを反映する必要があります。