Il sistema di tipi di C # manca di un paio di funzioni necessarie per implementare correttamente le classi di tipi come interfaccia.
Iniziamo con il tuo esempio, ma la chiave mostra un account più completo su cosa è e cosa fa un typeclass e poi prova a mappare quelli a C # bits.
class Functor f where
fmap :: (a -> b) -> f a -> f b
Questa è la definizione della classe del tipo o simile all'interfaccia. Ora esaminiamo una definizione di un tipo e la sua implementazione di tale classe di caratteri.
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 delle classi di tipi che non puoi avere con le interfacce. L'implementazione della classe del 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.
Questo è probabilmente il più grande immediatamente, ma c'è un'altra differenza abbastanza significativa che rende l'equivalente C # davvero non funzionante quasi altrettanto bene, e ci stai toccando nella tua domanda. Riguarda il polimorfismo. Inoltre ci sono alcune cose relativamente generiche che Haskell ti permette di fare con classi di tipi che non si traducono in modo particolare, specialmente quando inizi a guardare la quantità di generismo in tipi esistenziali o altre estensioni GHC come gli ADT generici.
Vedete, con Haskell potete definire i funtori
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 in 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 si scrive 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 c'è un paio di cose che non vanno nella versione C #, per una cosa non sono nemmeno sicuro che ti permetterà di usare il qualificatore <b>
come ho fatto io, ma senza di esso I am certo che non invierà Show<>
appropriatamente (sentiti libero di provare e compilare per scoprirlo, io no).
Il problema più grande qui però è che diversamente da sopra in Haskell dove avevamo il nostro Terminal
s definito come parte del tipo e quindi utilizzabile al posto del tipo, a causa del C # che non ha il polimorfismo parametrico appropriato (che diventa super ovvio come Appena proverai ad interopizzare F # con C #) non puoi distinguere chiaramente o chiaramente se Right o Left sono Terminal
s. La cosa migliore che puoi fare è usare null
, ma cosa succede se stai provando a fare un tipo di valore a Functor
o nel caso di Either
dove stai distinguendo due tipi che entrambi hanno un valore? Ora devi usare un tipo e avere due valori diversi da controllare e passare da un modello all'altro per discriminare?
La mancanza di veri e propri tipi di somma, tipi di unione, ADT, qualunque cosa tu voglia chiamarli fa davvero molto di ciò che i typeclass ti fanno svanire perché alla fine del giorno ti permettono di trattare più tipi (costruttori) come singolo tipo, e il sistema di tipo sottostante di .NET semplicemente non ha questo concetto.