Implémenter la classe de types Haskell avec une interface C #

13

J'essaie de comparer les classes de types de Haskell et les interfaces de C #. Supposons qu'il y ait un Functor.

Haskell:

class Functor f where
  fmap :: (a -> b) -> f a -> f b

Comment implémenter cette classe de type comme interface en C #?

Ce que j'ai essayé:

interface Functor<A, B>
{
    F<B> fmap(Func<A, B> f, F<A> x);
}

Ceci est une implémentation non valide et je suis en fait coincé avec un générique F type qui devrait être renvoyé par fmap. Comment le définir et où?

Est-il impossible d'implémenter Functoren C # et pourquoi? Ou peut-être existe-t-il une autre approche?

ДМИТРИЙ МАЛИКОВ
la source
8
Eric Lippert parle un peu de la façon dont le système de type C # n'est pas vraiment suffisant pour prendre en charge la nature de type supérieur des Functors telle que définie par Haskell dans cette réponse: stackoverflow.com/a/4412319/303940
KChaloux
1
C'était il y a environ 3 ans. Quelque chose a changé?
ДМИТРИЙ МАЛИКОВ
4
rien n'a changé pour rendre cela possible en C #, et je ne pense pas que ce soit probable à l'avenir
jk.

Réponses:

8

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 Terminaldé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 Terminals. 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 Eitheroù 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.

Jimmy Hoffa
la source
2
Je ne connais pas très bien Haskell (uniquement Standard ML), donc je ne sais pas à quel point cela fait une différence, mais il est possible d'encoder des types de somme en C # .
Doval
5

Vous avez besoin de deux classes, une pour modéliser le générique d'ordre supérieur (le foncteur) et l'autre pour modéliser le foncteur combiné avec la valeur libre A

interface F<Functor> {
   IF<Functor, A> pure<A>(A a);
}

interface IF<Functor, A> where Functor : F<Functor> {
   IF<Functor, B> pure<B>(B b);
   IF<Functor, B> map<B>(Func<A, B> f);
}

Donc, si nous utilisons l'option monade (car toutes les monades sont des foncteurs)

class Option : F<Option> {
   IF<Option, A> pure<A>(A a) { return new Some<A>(a) };
}

class OptionF<A> : IF<Option, A> {
   IF<Option, B> pure<B>(B b) {
      return new Some<B>(b);
   }

   IF<Option, B> map<B>(Func<A, B> f) {
       var some = this as Some<A>;
       if (some != null) {
          return new Some<B>(f(some.value));
       } else {
          return new None<B>();
       }
   } 
}

Vous pouvez ensuite utiliser des méthodes d'extension statiques pour convertir de IF <Option, B> en Some <A> lorsque vous devez

DetriusXii
la source
J'ai des difficultés avec purel'interface générique du foncteur: le compilateur se plaint IF<Functor, A> pure<A>(A a);de "Le type Functorne peut pas être utilisé comme paramètre de type Functordans le type de méthode générique IF<Functor, A>. Il n'y a pas de conversion de boxe ni de conversion de paramètre de type de Functorà F<Functor>". Qu'est-ce que ça veut dire? Et pourquoi faut-il définir pureen deux endroits? De plus, ne devrait pas pureêtre statique?
Niriel
1
Salut. Je pense parce que je faisais allusion aux monades et aux transformateurs de monade lors de la conception de la classe. Un transformateur de monade, comme le transformateur de monade OptionT (MaybeT dans Haskell) est défini en C # comme OptionT <M, A> où M est une autre monade générique. Le transformateur de monade OptionT se trouve dans une monade de type M <Option <A>>, mais comme C # n'a pas de types de type supérieur, vous avez besoin d'un moyen d'instancier la monade M de type supérieur lorsque vous appelez OptionT.map et OptionT.bind. Les méthodes statiques ne fonctionnent pas parce que vous ne pouvez pas appeler M.pure (A a) pour une monade M.
DetriusXii