Le système de type de C # manque de quelques fonctionnalités nécessaires pour implémenter correctement les classes de type en tant qu'interface.
Commençons par votre exemple, mais la clé montre un compte rendu plus complet de ce qu'est et fait une classe de types, puis essaie de les mapper en bits C #.
class Functor f where
fmap :: (a -> b) -> f a -> f b
Il s'agit de la définition de classe de type, ou similaire à l'interface. Examinons maintenant la définition d'un type et son implémentation de cette classe de type.
data Awesome a = Awesome a a
instance Functor Awesome where
fmap f (Awesome a1 a2) = Awesome (f a1) (f a2)
Maintenant, nous pouvons voir très clairement un fait distinct des classes de type que vous ne pouvez pas avoir avec des interfaces. L'implémentation de la classe type ne fait pas partie de la définition du type. En C #, pour implémenter une interface, vous devez l'implémenter dans le cadre de la définition du type qui l'implémente. Cela signifie que vous ne pouvez pas implémenter une interface pour un type que vous n'implémentez pas vous-même, mais dans Haskell, vous pouvez implémenter une classe de type pour tout type auquel vous avez accès.
C'est probablement le plus grand immédiatement, mais il y a une autre différence assez importante qui fait que l'équivalent C # ne fonctionne vraiment pas aussi bien, et vous le touchez dans votre question. Il s'agit du polymorphisme. Il y a aussi des choses relativement génériques que Haskell vous permet de faire avec des classes de types qui ne se traduisent pas, surtout lorsque vous commencez à regarder la quantité de générique dans les types existentiels ou d'autres extensions GHC comme les ADT génériques.
Vous voyez, avec Haskell vous pouvez définir les foncteurs
data List a = List a (List a) | Terminal
data Tree a = Tree val (Tree a) (Tree a) | Terminal
instance Functor List where
fmap :: (a -> b) -> List a -> List b
fmap f (List a Terminal) = List (f a) Terminal
fmap f (List a rest) = List (f a) (fmap f rest)
instance Functor Tree where
fmap :: (a -> b) -> Tree a -> Tree b
fmap f (Tree val Terminal Terminal) = Tree (f val) Terminal Terminal
fmap f (Tree val Terminal right) = Tree (f val) Terminal (fmap f right)
fmap f (Tree val left Terminal) = Tree (f val) (fmap f left) Terminal
fmap f (Tree val left right) = Tree (f val) (fmap f left) (fmap f right)
Ensuite, dans la consommation, vous pouvez avoir une fonction:
mapsSomething :: Functor f, Show a => f a -> f String
mapsSomething rar = fmap show rar
C'est ici que se trouve le problème. En C # comment écrivez-vous cette fonction?
public Tree<a> : Functor<a>
{
public a Val { get; set; }
public Tree<a> Left { get; set; }
public Tree<a> Right { get; set; }
public Functor<b> fmap<b>(Func<a,b> f)
{
return new Tree<b>
{
Val = f(val),
Left = Left.fmap(f);
Right = Right.fmap(f);
};
}
}
public string Show<a>(Showwable<a> ror)
{
return ror.Show();
}
public Functor<String> mapsSomething<a,b>(Functor<a> rar) where a : Showwable<b>
{
return rar.fmap(Show<b>);
}
Il y a donc quelques problèmes avec la version C #, d'une part je ne suis même pas certain que cela vous permettra d'utiliser le <b>
qualificatif comme je l'ai fait là-bas, mais sans cela, je suis certain qu'il ne serait pas distribué Show<>
correctement (n'hésitez pas à essayer et compiler pour le découvrir, je ne l'ai pas).
Le plus gros problème ici est cependant que contrairement à Haskell où nous avions Terminal
défini nos s comme faisant partie du type, puis utilisables à la place du type, en raison du manque de polymorphisme paramétrique approprié de C # (qui devient super évident dès que vous essayez d'interopérer F # avec C #), vous ne pouvez pas distinguer clairement ou proprement si Droite ou Gauche sont des Terminal
s. Le mieux que vous puissiez faire est d'utiliser null
, mais que faire si vous essayez de faire une valeur de type aFunctor
ou dans le cas Either
où vous distinguez deux types qui ont tous deux une valeur? Maintenant, vous devez utiliser un type et avoir deux valeurs différentes pour vérifier et basculer entre pour modéliser votre discrimination?
L'absence de types de somme appropriés, de types d'union, d'ADT, quel que soit le nom que vous voulez leur donner, fait vraiment perdre beaucoup de ce que les classes de types vous donnent parce qu'en fin de compte, elles vous permettent de traiter plusieurs types (constructeurs) comme un seul type, et le système de type sous-jacent de .NET n'a tout simplement pas un tel concept.