Implementa la tabella dei tipi Haskell con interfaccia C #


13

Sto cercando di confrontare le classi di tipi di Haskell e le interfacce di C #. Supponiamo che ci sia un Functor.

Haskell:

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

Come implementare questa classe di tipi come interfaccia in C #?

Cosa ho provato:

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

Questa è un'implementazione non valida e in realtà sono bloccato con un Ftipo generico che dovrebbe essere restituito da fmap. Come dovrebbe essere definito e dove?

È impossibile implementarlo Functorin C # e perché? O forse c'è un altro approccio?


8
Eric Lippert parla un po 'su come C #' s sistema di tipo non è davvero sufficiente a sostenere la natura superiore kinded di Funtori come definito da Haskell in questa risposta: stackoverflow.com/a/4412319/303940
KChaloux

1
Questo è stato circa 3 anni fa. Qualcosa è cambiato?
ДМИТРИЙ МАЛИКОВ

4
non è cambiato nulla per renderlo possibile in C #, né penso che sia probabile in futuro
jk.

Risposte:


8

Il sistema di tipi di C # manca di un paio di funzioni necessarie per implementare correttamente le classi di tipi come interfaccia.

Cominciamo con il tuo esempio, ma la chiave sta mostrando un resoconto più completo di ciò che è e fa una typeclass, e quindi prova a mappare quelli su bit C #.

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

Questa è la definizione della classe di tipo o simile all'interfaccia. Vediamo ora una definizione di un tipo e la sua implementazione di quella classe di tipi.

data Awesome a = Awesome a a

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

Ora possiamo vedere molto chiaramente un fatto distinto di classi di tipi che non puoi avere con le interfacce. L'implementazione della classe di tipo non fa parte della definizione del tipo. In C #, per implementare un'interfaccia, è necessario implementarla come parte della definizione del tipo che la implementa. Ciò significa che non è possibile implementare un'interfaccia per un tipo che non si implementa da soli, tuttavia in Haskell è possibile implementare una classe di tipo per qualsiasi tipo a cui si ha accesso.

Questa è probabilmente la più grande immediatamente, ma c'è un'altra differenza abbastanza significativa che rende l'equivalente C # in realtà non funziona altrettanto bene e lo stai toccando nella tua domanda. Riguarda il polimorfismo. Inoltre ci sono alcune cose relativamente generiche che Haskell ti permette di fare con le classi di tipi che non traducono, soprattutto quando inizi a guardare la quantità di generalità in tipi esistenziali o altre estensioni GHC come gli ADT generici.

Vedi, con Haskell puoi definire i funzione

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)

Quindi nel consumo puoi avere una funzione:

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

Qui sta il problema. In C # come scrivi questa funzione?

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

Quindi ci sono un paio di cose che non vanno nella versione C #, per prima cosa non sono nemmeno sicuro che ti permetterà di usare il <b>qualificatore come ho fatto lì, ma senza di esso sono certo che non invierà in modo Show<>appropriato (sentiti libero di provare e compilare per scoprirlo; non l'ho fatto).

Il problema più grande qui, tuttavia, è che a differenza di sopra in Haskell dove abbiamo Terminaldefinito i nostri s come parte del tipo e quindi utilizzabili al posto del tipo, a causa della mancanza di un appropriato polimorfismo parametrico in C # (che diventa super evidente non appena si tenta di interagire F # con C #) non è possibile distinguere chiaramente o chiaramente se TerminalD o Sinistra sono s. La cosa migliore che puoi fare è usare null, ma cosa succede se stai cercando di creare un valore digita a Functoro nel caso in Eithercui stai distinguendo due tipi che portano entrambi un valore? Ora devi usare un tipo e avere due valori diversi per verificare e passare da un modello all'altro per discriminare?

La mancanza di tipi di somma adeguati, tipi di unione, ADT, qualunque cosa tu voglia chiamarli fa davvero molto di ciò che ti danno le macchine da scrivere perché alla fine della giornata ti permettono di trattare più tipi (costruttori) come un singolo tipo, e il sistema di tipi sottostante .NET non ha semplicemente tale concetto.


2
Non sono molto esperto in Haskell (solo Standard ML), quindi non so quanta differenza ci sia, ma è possibile codificare i tipi di somma in C # .
Doval,

5

Ciò di cui hai bisogno sono due classi, una per modellare il generico di ordine superiore (il functor) e una per modellare il functor combinato con il valore libero 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);
}

Quindi se usiamo la monade Opzione (perché tutte le monadi sono funzioni)

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

È quindi possibile utilizzare i metodi di estensione statica per convertire da IF <Opzione, B> a Alcuni <A> quando è necessario


Ho delle difficoltà purenell'interfaccia generica del funzione: il compilatore si lamenta IF<Functor, A> pure<A>(A a);con "Il tipo Functornon può essere usato come parametro di tipo Functornel tipo generico di metodo IF<Functor, A>. Non esiste conversione di boxe o conversione di parametri di tipo da Functora F<Functor>". Cosa significa questo? E perché dobbiamo definire purein due punti? Inoltre, non dovrebbe pureessere statico?
Niriel,

1
Ciao. Penso perché stavo suggerendo monadi e trasformatori di monade durante la progettazione della classe. Un trasformatore di monade, come il trasformatore di monade OptionT (MaybeT in Haskell) è definito in C # come OptionT <M, A> dove M è un'altra monade generica. Il trasformatore della monade OptionT si inserisce in una monade di tipo M <Opzione <A>>, ma poiché C # non ha tipi di tipo superiore, è necessario un modo per creare un'istanza della monade M di tipo superiore quando si chiama OptionT.map e OptionT.bind. I metodi statici non funzionano perché non puoi chiamare M.pure (A a) per nessuna monade M.
DetriusXii
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.