Реалізуйте тип класу 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
Ерік Ліпперт говорить трохи про те , як С # тип системи не дійсно досить , щоб підтримувати більш kinded характер функторів , як це визначено Haskell в цій відповіді: 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визначали свою частину типу, а потім використовували її замість цього типу, через відсутність у C # відповідного параметричного поліморфізму (що стає надзвичайно очевидним, як тільки ви намагаєтесь інтеропувати F # з C #) ви не можете чітко чи чітко розрізнити, чи Право чи Ліво Terminals. Найкраще, що ви можете зробити, це використовувати null, але що робити , якщо ви намагаєтеся створити тип значення a Functorабо у випадку, Eitherколи ви розрізняєте два типи, які мають обоє значення? Тепер ви повинні використовувати один тип і мати два різних значення для перевірки та переключення між моделями вашої дискримінації?

Відсутність належних типів суми, типів об'єднань, ADT, як би ви їх не хотіли називати, дуже багато з того, що типи класів дають вам відпасти, оскільки в кінці дня вони дозволяють вам ставитися до декількох типів (конструкторів) як до одного типу, та система базового типу .NET просто не має такої концепції.


2
Я не дуже добре розбираюся в Haskell (тільки стандартний 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);
}

Отже, якщо ми використовуємо монаду Варіантів (адже всі монади - функтори)

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>, коли потрібно


У мене виникають труднощі з 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 <Опція <A>>, але оскільки у C # немає типів вищого роду, вам потрібен спосіб інстанціювати вищу М-монаду вищого роду при виклику OptionT.map та OptionT.bind. Статичні методи не працюють, тому що ви не можете викликати M.pure (A a) для жодної монади M.
DetriusXii
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.