Próbuję porównać klasy typów Haskella i interfejsy C #. Załóżmy, że istnieje Functor
.
Haskell:
class Functor f where
fmap :: (a -> b) -> f a -> f b
Jak zaimplementować tę klasę typu jako interfejs w języku C #?
Co próbowałem:
interface Functor<A, B>
{
F<B> fmap(Func<A, B> f, F<A> x);
}
To jest nieprawidłowa implementacja i faktycznie utknąłem z F
typem ogólnym, który powinien zostać zwrócony fmap
. Jak należy to określić i gdzie?
Czy niemożliwe jest wdrożenie Functor
w języku C # i dlaczego? A może istnieje inne podejście?
Odpowiedzi:
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 #.
Jest to definicja klasy typu lub podobna do interfejsu. Teraz spójrzmy na definicję typu i jego implementację tej klasy.
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
Następnie w zużyciu możesz mieć funkcję:
Na tym polega problem. W C # jak piszesz tę funkcję?
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łbyShow<>
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 toTerminal
s. Najlepsze, co możesz zrobić, to użyćnull
, ale co, jeśli próbujesz utworzyć typ wartości aFunctor
lub w przypadkuEither
, 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.
źródło
Potrzebne są dwie klasy, jedna do modelowania generycznego wyższego rzędu (funktor), a druga do modelowania połączonego funktora z dowolną wartością A
Więc jeśli użyjemy opcji monada (ponieważ wszystkie monady są funktorami)
Następnie można użyć metod rozszerzania statycznego, aby w razie potrzeby przekonwertować z JEŻELI <Opcja, B> na Niektóre <A>
źródło
pure
ogólnym interfejsem funktora: kompilator narzeka naIF<Functor, A> pure<A>(A a);
„TypFunctor
nie może być użyty jako parametr typuFunctor
w ogólnej metodzieIF<Functor, A>
. Nie ma konwersji boksu ani konwersji typu zFunctor
naF<Functor>
”. Co to znaczy? I dlaczego musimy definiowaćpure
w dwóch miejscach? Co więcej, nie powinnopure
być statyczne?