Zaimplementuj typ Haskell z interfejsem C #

13

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 Ftypem ogólnym, który powinien zostać zwrócony fmap. Jak należy to określić i gdzie?

Czy niemożliwe jest wdrożenie Functorw języku C # i dlaczego? A może istnieje inne podejście?

ДМИТРИЙ МАЛИКОВ
źródło
8
Eric Lippert mówi trochę o tym, że system typów C # nie jest tak naprawdę wystarczający, aby obsługiwać bardziej przyjazną naturę Functors, jak zdefiniował Haskell w tej odpowiedzi: stackoverflow.com/a/4412319/303940
KChaloux
1
To było około 3 lata temu. Coś się zmieniło?
ДМИТРИЙ МАЛИКОВ
4
nic się nie zmieniło, aby było to możliwe w języku C #, i nie sądzę, że będzie to prawdopodobnie w przyszłości
jk.

Odpowiedzi:

8

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 Terminalzdefiniowaliś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 Terminals. Najlepsze, co możesz zrobić, to użyć null, ale co, jeśli próbujesz utworzyć typ wartości a Functorlub 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.

Jimmy Hoffa
źródło
2
Nie jestem zbyt dobrze zaznajomiony z Haskellem (tylko Standard ML), więc nie wiem, jaka to różnica, ale możliwe jest kodowanie typów sum w C # .
Doval,
5

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

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

Więc jeśli użyjemy opcji monada (ponieważ wszystkie monady są funktorami)

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

Następnie można użyć metod rozszerzania statycznego, aby w razie potrzeby przekonwertować z JEŻELI <Opcja, B> na Niektóre <A>

DetriusXii
źródło
Mam trudności z pureogólnym interfejsem funktora: kompilator narzeka na IF<Functor, A> pure<A>(A a);„Typ Functornie może być użyty jako parametr typu Functorw ogólnej metodzie IF<Functor, A>. Nie ma konwersji boksu ani konwersji typu z Functorna F<Functor>”. Co to znaczy? I dlaczego musimy definiować purew dwóch miejscach? Co więcej, nie powinno purebyć statyczne?
Niriel,
1
Cześć. Chyba dlatego, że przy projektowaniu klasy podpowiedziałem do monad i transformatorów monad. Transformator monadowy, podobnie jak transformator monadowy OptionT (możeT w Haskell) jest zdefiniowany w języku C # jako OptionT <M, A>, gdzie M jest inną ogólną monadą. Transformator monadowy OptionT montuje się w monadzie typu M <Opcja <A>>, ale ponieważ C # nie ma wyższych rodzajów sortowanych, potrzebujesz sposobu na utworzenie instancji wyższej monady M podczas wywoływania OptionT.map i OptionT.bind. Metody statyczne nie działają, ponieważ nie można wywołać M.pure (A a) dla żadnej monady M.
DetriusXii