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 Terminal
definito 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 Terminal
D o Sinistra sono s. La cosa migliore che puoi fare è usare null
, ma cosa succede se stai cercando di creare un valore digita a Functor
o nel caso in Either
cui 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.