Реализовать класс типов Haskell с интерфейсом C #

13

Я пытаюсь сравнить классы типов Haskell и интерфейсы C #. Предположим, что есть Functor.

Haskell:

class Functor f where
  fmap :: (a -> b) -> f a -> f b

Как реализовать этот тип класса в качестве интерфейса в C #?

Что я пробовал:

interface Functor<A, B>
{
    F<B> fmap(Func<A, B> f, F<A> x);
}

Это неверная реализация, и я на самом деле застрял с универсальным Fтипом, который должен быть возвращен fmap. Как это должно быть определено и где?

Это невозможно реализовать Functorв C # и почему? Или, может быть, есть другой подход?

ДМИТРИЙ МАЛИКОВ
источник
8
Эрик Липперт немного говорит о том, что системы типов C # на самом деле недостаточно для поддержки более высокой природы Функторов, как это определено Хаскеллом в этом ответе: stackoverflow.com/a/4412319/303940
KChaloux
1
Это было около 3 лет назад. Что-то изменилось?
ДМИТРИЙ МАЛИКОВ
4
ничего не изменилось, чтобы сделать это возможным в C #, и я не думаю, что это произойдет в будущем
jk.

Ответы:

8

В системе типов C # отсутствует пара функций, необходимых для правильной реализации классов типов в качестве интерфейса.

Давайте начнем с вашего примера, но ключ показывает более полное описание того, что такое класс типов и что делает, а затем пытается сопоставить их с битами C #.

class Functor f where
  fmap :: (a -> b) -> f a -> f b

Это определение класса типа или аналог интерфейса. Теперь давайте посмотрим на определение типа и его реализацию класса этого типа.

data Awesome a = Awesome a a

instance Functor Awesome where
  fmap f (Awesome a1 a2) = Awesome (f a1) (f a2)

Теперь мы можем ясно видеть один особый факт классов типов, которые вы не можете иметь с интерфейсами. Реализация класса типа не является частью определения типа. В C # для реализации интерфейса вы должны реализовать его как часть определения типа, который его реализует. Это означает, что вы не можете реализовать интерфейс для типа, который вы не реализуете самостоятельно, однако в Haskell вы можете реализовать класс типа для любого типа, к которому у вас есть доступ.

Это, вероятно, самое большое сразу, но есть еще одно довольно существенное отличие, которое делает эквивалент C # действительно не слишком эффективным, и вы затрагиваете его в своем вопросе. Это о полиморфизме. Также есть некоторые относительно общие вещи, которые Haskell позволяет вам делать с классами типов, которые прямо не переводятся, особенно когда вы начинаете смотреть на количество обобщений в экзистенциальных типах или других расширениях GHC, таких как Generic ADT.

Видите ли, с Haskell вы можете определить функторы

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)

Тогда в потреблении вы можете иметь функцию:

mapsSomething :: Functor f, Show a => f a -> f String
mapsSomething rar = fmap show rar

В этом и заключается проблема. В C # как вы пишете эту функцию?

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

Таким образом , есть пара вещей , которые не так с C # версии, с одной стороны , я даже не уверен , что это позволит использовать <b>спецификатор , как я сделал, но без него я буду уверен , что это не будет рассылать Show<>надлежащим образом ( не стесняйтесь попробовать и собрать, чтобы узнать; я не сделал).

Однако, здесь большая проблема заключается в том, что в отличие от вышеописанного в Haskell, где мы Terminalопределили наши s как часть типа и затем использовали их вместо типа, из-за отсутствия в C # соответствующего параметрического полиморфизма (который становится супер очевидным, как только вы пытаетесь взаимодействовать F # с C #) вы не можете четко или четко различать, являются ли Right или Left Terminals. Лучшее, что вы можете сделать, это использовать null, но что, если вы пытаетесь создать тип значения a Functorили в случае, Eitherкогда вы различаете два типа, оба из которых имеют значение? Теперь вы должны использовать один тип и иметь два разных значения для проверки и переключения между ними для моделирования вашей дискриминации?

Отсутствие правильных типов сумм, типов объединений, ADT, как бы вы их ни называли, на самом деле делает многое из того, что дают вам классы типов, потому что в конце дня они позволяют вам рассматривать несколько типов (конструкторов) как один тип, и базовая система типов .NET просто не имеет такой концепции.

Джимми Хоффа
источник
2
Я не очень хорошо разбираюсь в Haskell (только Standard ML), поэтому я не знаю, насколько это важно , но возможно кодировать типы сумм в C # .
Довал
5

Вам нужны два класса: один для моделирования обобщенного функтора более высокого порядка (функтор), а второй - для свободного комбинированного значения 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);
}

Так что, если мы используем монаду Option (потому что все монады являются функторами)

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

Затем вы можете использовать методы статического расширения для преобразования из IF <Option, B> в Some <A>, когда вам нужно

DetriusXii
источник
У меня возникают трудности с pureинтерфейсом универсального функтора: компилятор жалуется на IF<Functor, A> pure<A>(A a);«Тип Functorне может использоваться в качестве параметра Functorтипа в методе универсального типа IF<Functor, A>. Нет преобразования в бокс или преобразования параметра типа из Functorв F<Functor>». Что это значит? И почему мы должны определить pureв двух местах? Кроме того, не должно pureбыть статичным?
Нириэль
1
Здравствуй. Я думаю, потому что я намекал на монады и монадные трансформаторы, проектируя класс. Преобразователь монад, такой как преобразователь монад OptionT (MaybeT в Haskell), определен в C # как OptionT <M, A>, где M - другая общая монада. Преобразователь монады OptionT включает монаду типа M <Option <A >>, но поскольку в C # нет типов с более высоким родом, вам нужен способ создания экземпляра монады с более высоким родом при вызове OptionT.map и OptionT.bind. Статические методы не работают, потому что вы не можете вызвать M.pure (A a) для любой монады M.
DetriusXii