Implementieren Sie die Haskell-Typenklasse mit der C # -Schnittstelle

13

Ich versuche, Haskells Typklassen und C # -Schnittstellen zu vergleichen. Angenommen, es gibt eine Functor.

Haskell:

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

Wie implementiere ich diese Typklasse als Schnittstelle in C #?

Was ich ausprobiert habe:

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

Dies ist eine ungültige Implementierung und ich bin tatsächlich mit generischen FTypen festgefahren , die von zurückgegeben werden sollten fmap. Wie und wo soll es definiert werden?

Ist es unmöglich, Functorin C # zu implementieren und warum? Oder gibt es vielleicht einen anderen Ansatz?

ДМИТРИЙ МАЛИКОВ
quelle
8
Eric Lippert spricht ein wenig darüber, dass das Typsystem von C # nicht ausreicht, um die von Haskell definierte höhere Art von Functors zu unterstützen: stackoverflow.com/a/4412319/303940
KChaloux
1
Das war vor ungefähr 3 Jahren. Etwas hat sich verändert?
ДМИТРИЙ МАЛИКОВ
4
Weder hat sich etwas geändert, um dies in C # zu ermöglichen, noch denke ich, dass dies in Zukunft wahrscheinlich sein wird
jk.

Antworten:

8

Dem Typensystem von C # fehlen einige Funktionen, die zum ordnungsgemäßen Implementieren von Typklassen als Schnittstelle erforderlich sind.

Beginnen wir mit Ihrem Beispiel, aber der Schlüssel zeigt eine ausführlichere Darstellung dessen, was eine Typenklasse ist und tut, und versucht dann, diese C # -Bits zuzuordnen.

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

Dies ist die Typklassendefinition oder ähnlich der Schnittstelle. Betrachten wir nun die Definition eines Typs und die Implementierung dieser Typklasse.

data Awesome a = Awesome a a

instance Functor Awesome where
  fmap f (Awesome a1 a2) = Awesome (f a1) (f a2)

Jetzt können wir ganz offensichtlich eine eindeutige Tatsache von Typklassen erkennen, die Sie mit Interfaces nicht haben können. Die Implementierung der Typklasse ist nicht Bestandteil der Typdefinition. Um eine Schnittstelle in C # zu implementieren, müssen Sie sie als Teil der Definition des Typs implementieren, der sie implementiert. Dies bedeutet, dass Sie möglicherweise keine Schnittstelle für einen Typ implementieren, den Sie selbst nicht implementieren. In Haskell können Sie jedoch eine Typklasse für jeden Typ implementieren, auf den Sie Zugriff haben.

Das ist wahrscheinlich der größte sofort, aber es gibt einen weiteren ziemlich signifikanten Unterschied, der bewirkt, dass das C # -Äquivalent bei weitem nicht so gut funktioniert, und Sie berühren ihn in Ihrer Frage. Es geht um Polymorphismus. Außerdem gibt es einige relativ generische Dinge, die Haskell Ihnen ermöglicht, mit Typklassen umzugehen, die von vornherein nicht übersetzt werden, insbesondere wenn Sie sich mit dem Ausmaß des Generizismus in existenziellen Typen oder anderen GHC-Erweiterungen wie generischen ADTs befassen.

Sie sehen, mit Haskell können Sie die Funktoren definieren

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)

Dann können Sie im Verbrauch eine Funktion haben:

mapsSomething :: Functor f, Show a => f a -> f String
mapsSomething rar = fmap show rar

Hierin liegt das Problem. Wie schreibt man diese Funktion in C #?

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>);
}

So gibt es ein paar Dinge falsch mit dem C # Version, für eine Sache , ich bin nicht einmal sicher , es erlaubt Ihnen , das zu verwenden , <b>Qualifier , wie ich es tat, aber ohne es ich bin sicher , es würde nicht versenden Show<>angemessen (fühlen sich frei , um zu versuchen , und kompilieren, um herauszufinden; habe ich nicht).

Das größere Problem hierbei ist jedoch, dass anders als oben in Haskell, wo wir unsere Terminals als Teil des Typs definiert und dann anstelle des Typs verwendbar hatten, weil C # keinen geeigneten parametrischen Polymorphismus aufweist (was sehr offensichtlich wird, sobald Sie versuchen, ein Interop durchzuführen F # mit C #) kann man nicht klar oder sauber unterscheiden, ob Rechts oder Links Terminals sind. Das Beste null, was Sie tun können, ist die Verwendung , aber was ist, wenn Sie versuchen, einen Wert als Typ a festzulegen, Functoroder wenn Sie Eitherzwei Typen unterscheiden, die beide einen Wert enthalten? Jetzt müssen Sie einen Typ und zwei verschiedene Werte verwenden, um zu überprüfen und zwischen diesen zu wechseln, um Ihre Diskriminierung zu modellieren.

Das Fehlen geeigneter Summentypen, Vereinigungstypen und ADTs, wie auch immer Sie sie nennen möchten, führt dazu, dass viele der Typklassen verloren gehen, da Sie am Ende des Tages mehrere Typen (Konstruktoren) als einen Typ behandeln können. und das zugrunde liegende Typensystem von .NET hat einfach kein solches Konzept.

Jimmy Hoffa
quelle
2
Ich bin mit Haskell (nur Standard ML) nicht sehr vertraut, daher weiß ich nicht, welchen Unterschied dies macht, aber es ist möglich, Summentypen in C # zu codieren .
Doval
5

Was Sie brauchen, sind zwei Klassen, eine zur Modellierung des Generikums höherer Ordnung (des Funktors) und eine zur Modellierung des kombinierten Funktors mit dem freien Wert 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);
}

Wenn wir also die Option monad verwenden (weil alle Monaden Funktoren sind)

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>();
       }
   } 
}

Sie können dann statische Erweiterungsmethoden verwenden, um bei Bedarf von IF <Option, B> nach Some <A> zu konvertieren

DetriusXii
quelle
Ich habe Probleme mit pureder generischen Funktionsoberfläche: Der Compiler beschwert sich über IF<Functor, A> pure<A>(A a);"Der Typ Functorkann nicht als Typparameter Functorin der generischen Methode verwendet werden IF<Functor, A>. Es findet keine Boxing-Konvertierung oder Typparameter-Konvertierung von Functornach statt F<Functor>." Was bedeutet das? Und warum müssen wir purean zwei Stellen definieren ? Darüber hinaus sollte nicht purestatisch sein?
Niriel
1
Hallo. Ich denke, weil ich beim Entwerfen der Klasse auf Monaden und Monadentransformatoren hingewiesen habe. Ein Monadentransformator wie der OptionT-Monadentransformator (MaybeT in Haskell) wird in C # als OptionT <M, A> definiert, wobei M eine andere generische Monade ist. Die OptionT-Monadentransformatorboxen in einer Monade vom Typ M <Option <A >>. Da C # jedoch keine höherwertigen Typen hat, müssen Sie beim Aufrufen von OptionT.map und OptionT.bind die höherwertigen M-Monaden instanziieren. Statische Methoden funktionieren nicht, weil Sie M.pure (A a) für keine Monade M.
DetriusXii 12.09.16