W systemie typów C # brakuje kilku funkcji niezbędnych do prawidłowego wdrożenia klas typów jako interfejsu.
Zacznijmy od twojego przykładu, ale kluczem jest pokazanie pełniejszego opisu tego, czym jest i czym jest typeclas, a następnie próba mapowania ich na bity C #.
class Functor f where
fmap :: (a -> b) -> f a -> f b
Jest to definicja klasy typu lub podobna do interfejsu. Teraz spójrzmy na definicję typu i jego implementację tej klasy.
data Awesome a = Awesome a a
instance Functor Awesome where
fmap f (Awesome a1 a2) = Awesome (f a1) (f a2)
Teraz widzimy bardzo wyraźnie jeden wyraźny fakt klas typów, którego nie można mieć w interfejsach. Implementacja klasy typu nie jest częścią definicji typu. W języku C #, aby zaimplementować interfejs, należy go zaimplementować jako część definicji typu, który go implementuje. Oznacza to, że nie możesz zaimplementować interfejsu dla typu, którego sam nie wdrażasz, jednak w Haskell możesz zaimplementować klasę typu dla dowolnego typu, do którego masz dostęp.
To prawdopodobnie największa natychmiast, ale jest jeszcze jedna dość znacząca różnica, która sprawia, że odpowiednik C # naprawdę nie działa tak dobrze, a ty dotykasz go w swoim pytaniu. Chodzi o polimorfizm. Jest też kilka względnie ogólnych rzeczy, które Haskell pozwala robić z klasami typów, które wprost nie tłumaczą, szczególnie gdy zaczynasz patrzeć na ilość generyczności w typach egzystencjalnych lub innych rozszerzeniach GHC, takich jak Generyczne ADT.
Widzisz, dzięki Haskell możesz zdefiniować funktory
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)
Następnie w zużyciu możesz mieć funkcję:
mapsSomething :: Functor f, Show a => f a -> f String
mapsSomething rar = fmap show rar
Na tym polega problem. W C # jak piszesz tę funkcję?
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>);
}
Jest więc kilka rzeczy nie tak z wersją C #, z jednej strony nie jestem nawet pewien, że pozwoli ci użyć <b>
kwalifikatora tak, jak tam zrobiłem, ale bez tego jestem pewien, że nie wysłałby Show<>
odpowiednio (wypróbuj i skompiluj, aby się dowiedzieć; nie zrobiłem tego).
Większy problem polega jednak na tym, że w przeciwieństwie do powyższego w Haskell, gdzie Terminal
zdefiniowaliśmy nasze s jako część typu, a następnie można je zastąpić typem, ponieważ C # nie ma odpowiedniego polimorfizmu parametrycznego (co staje się super oczywiste, gdy tylko spróbujesz interop F # z C #) nie można jednoznacznie lub wyraźnie odróżnić, czy prawa czy lewa to Terminal
s. Najlepsze, co możesz zrobić, to użyć null
, ale co, jeśli próbujesz utworzyć typ wartości a Functor
lub w przypadku Either
, gdy wyróżniasz dwa typy, które niosą wartość? Teraz musisz użyć jednego typu i mieć dwie różne wartości, aby sprawdzić i przełączać się między modelami dyskryminacji?
Brak odpowiednich typów sum, typów unii, ADT, jakkolwiek chcesz je nazywać, naprawdę sprawia, że wiele typów pada od ciebie, ponieważ pod koniec dnia pozwalają ci traktować wiele typów (konstruktorów) jako jeden typ, a bazowy system typów .NET po prostu nie ma takiej koncepcji.