J'essaie actuellement de comprendre les classes et les instances et je ne comprends pas encore pourquoi. J'ai deux questions sur le sujet jusqu'à présent:
1) Pourquoi est-il nécessaire d'avoir une classe de type dans une signature de fonction lorsque la fonction utilise une fonction de cette classe de type? Exemple:
f :: (Eq a) => a -> a -> Bool
f a b = a == b
Pourquoi mettre (Eq a)
dans la signature. Si ==
n'est pas défini pour a
, pourquoi ne pas simplement renvoyer l'erreur en rencontrant a == b
? Quel est l'intérêt de devoir déclarer la classe de type devant?
2) Quel est le lien entre les classes de types et la surcharge de fonctions?
Ce n'est pas possible de faire ceci:
data A = A
data B = B
f :: A -> A
f a = a
f :: B -> B
f b = b
Mais il est possible de faire ceci:
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
Qu'est-ce qui se passe avec ça? Pourquoi ne puis-je pas avoir deux fonctions portant le même nom mais opérant sur différents types ... Venant du C++, je trouve cela très étrange. Mais j'ai probablement de fausses conceptions sur ce que sont vraiment ces choses. mais une fois que je les enveloppe dans ces choses d'instance de classe de type je peux.
N'hésitez pas à me lancer une catégorie ou à taper des mots théoriques également, car j'apprends ces sujets parallèlement à l'apprentissage de Haskell et je soupçonne qu'il existe un fondement théorique à leur fonctionnement.
En bref: car c’est ainsi que Haskell a été conçu .
Pourquoi mettre
(Eq a)
dans la signature. Si==
n'est pas défini pour un, pourquoi ne pas simplement renvoyer l'erreur en rencontranta == b
?
Pourquoi plaçons-nous les types dans la signature d'un programme C++ (et pas seulement comme une affirmation dans le corps)? Parce que c'est comme ça que C++ est conçu. Typiquement, un concept sur lequel les langages de programmation sont construits est "explicite ce qui doit être explicite".
Il n'est pas dit qu'un module Haskell est open-source. Cela signifie donc que nous n’avons que la signature disponible. Cela voudrait donc dire que lorsque nous écrivons par exemple:
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
Nous écririons fréquemment ici foo
avec des types qui n'ont pas de classe Eq
. En conséquence, nous aurions beaucoup d'erreurs qui ne seraient découvertes qu'au moment de la compilation (ou si Haskell était un langage dynamique, au moment de l'exécution). L'idée de mettre Eq a
dans la signature de type est que nous pouvons rechercher la signature de foo
à l'avance, et ainsi nous assurer que les types sont des instances de la classe de types.
Notez qu'il n'est pas nécessaire d'écrire vous-même les signatures de type: Haskell peut généralement dériver la signature d'une fonction, mais une signature doit inclure toutes les informations nécessaires pour appeler et utiliser efficacement une fonction. En ajoutant des contraintes de type, nous accélérons le développement.
Qu'est-ce qui se passe avec ça? Pourquoi ne puis-je pas avoir deux fonctions du même nom mais fonctionnant sur des types différents?.
Encore une fois: c'est comme ça que Haskell est conçu. Les fonctions des langages de programmation fonctionnels sont "citoyens de première classe". Cela signifie que ceux-ci ont généralement un nom et nous voulons éviter autant que possible les conflits de noms. Tout comme les classes en C++ ont généralement un nom unique (à l'exception des espaces de noms).
Supposons que vous définissiez deux fonctions différentes:
incr :: Int -> Int
incr = (+1)
incr :: Bool -> Bool
incr _ = True
bar = incr
Alors quelle incr
bar
devrait-elle choisir? Bien sûr, nous pouvons rendre les types explicites (c.-à-d. incr :: Bool -> Bool
), mais nous voulons généralement éviter ce travail, car cela introduit beaucoup de bruit.
Une autre bonne raison de ne pas le faire est qu’une classe de type n’est pas simplement un ensemble de fonctions: elle ajoute contracts à ces fonctions. Par exemple, la classe Monad
doit satisfaire certaines relations entre les fonctions. Par exemple, (>>= return)
devrait être équivalent à id
. En d'autres termes, la classe de types:
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m a
Ne décrit pas deux indépendantes _ fonctions (>>=)
et return
: il s'agit d'un ensemble de fonctions. Vous les avez tous les deux (généralement avec certains contrats entre les >>=
et return
), ou aucun d'entre eux.
Cela ne répond qu'à la question 1 (directement, au moins).
La signature de type f :: a -> a -> Bool
est un raccourci pour f :: forall a. a -> a -> Bool
. f
ne fonctionnerait vraiment pas pour tous les typesa
si cela ne fonctionne que pour a
s pour lesquels (==)
est défini. Cette restriction aux types qui ont (==)
est exprimée à l'aide de la contrainte (Eq a)
dans f :: forall a. (Eq a) => a -> a -> Bool
.
"Pour tous"/la quantification universelle est au cœur du polymorphisme (paramétrique) de Haskell et fournit, entre autres, les propriétés puissantes et importantes de parametricity .
Haskell tient à deux axiomes (entre autres):
Si tu avais
f :: A -> A
et
f :: B -> B
alors, selon les principes adoptés dans Haskell, f
serait toujours une expression valide, qui devrait toujours avoir un type single . Bien qu'il soit possible de faire cela en utilisant un sous-typage, cela a été jugé beaucoup plus compliqué que la solution de type.
De même, la nécessité de Eq a
dans
(==) :: Eq a => a -> a -> Bool
vient du fait que le type de ==
doit décrire complètement ce que vous pouvez en faire. Si vous ne pouvez l'appeler que pour certains types, la signature de type doit en tenir compte.