Введіть класи проти об’єктних інтерфейсів


33

Я не думаю, що я розумію типи класів. Я десь читав, що мислення про типові класи як "інтерфейси" (від OO), що тип реалізує, є неправильним та оманливим. Проблема полягає в тому, що у мене виникають проблеми бачити їх як щось інше, і як це неправильно.

Наприклад, якщо у мене клас типу (у синтаксисі Haskell)

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

Чим це відрізняється від інтерфейсу [1] (у синтаксисі Java)

interface Functor<A> {
  <B> Functor<B> fmap(Function<B, A> fn)
}

interface Function<Return, Argument> {
  Return apply(Argument arg);
}

Я можу подумати про те, що реалізація класу типів, яка використовується при певному виклику, не задається, а визначається з оточення - скажімо, вивчаючи доступні модулі для реалізації для цього типу. Це здається артефактом впровадження, який можна вирішити мовою ОО; як-от компілятор (або час виконання) міг би шукати обгортку / розширювач / monkey-patcher, яка виставляє необхідний інтерфейс для типу.

Що я пропускаю?

[1] Зверніть увагу, що f aаргумент вилучено з того fmapмоменту, як це є мовою ОО, ви будете викликати цей метод на об'єкті. Цей інтерфейс передбачає, що f aаргумент виправлено.

Відповіді:


46

У своїй базовій формі типи класів дещо схожі на об’єктні інтерфейси. Однак багато в чому вони набагато загальніші.

  1. Відправка здійснюється на типи, а не на значення. Для його виконання не потрібне значення. Наприклад, можна зробити диспетчеризацію результату типу функції, як у Readкласі Haskell :

    class Read a where
      readsPrec :: Int -> String -> [(a, String)]
      ...
    

    Таке відправлення явно неможливо в звичайних ОО.

  2. Класи типів, природно, поширюються на декілька відправок, просто надаючи кілька параметрів:

    class Mul a b c where
      (*) :: a -> b -> c
    
    instance Mul Int Int Int where ...
    instance Mul Int Vec Vec where ...
    instance Mul Vec Vec Int where ...
    
  3. Визначення екземплярів не залежать від визначень класів і типів, що робить їх більш модульними. Тип T з модуля A може бути модернізований до класу C з модуля M2, не змінюючи визначення будь-якого, просто надаючи екземпляр у модулі M3. Для OO це вимагає більше езотеричних (і менше OO-ish) функцій мови, таких як методи розширення.

  4. Класи типів засновані на параметричному поліморфізмі, а не підтипу. Це дозволяє більш точно вводити текст. Розглянемо напр

    pick :: Enum a => a -> a -> a
    pick x y = if fromEnum x == 0 then y else x
    

    vs.

    pick(x : Enum, y : Enum) : Enum = if x.fromEnum() == 0 then y else x
    

    У першому випадку застосування pick '\0' 'x'має тип Char, тоді як у другому випадку все, що б ви знали про результат, полягало б у тому, що це Enum. (Це також причина того, що більшість мов ООС в наші дні інтегрує параметричний поліморфізм.)

  5. Тісно пов'язане питання бінарних методів. Вони абсолютно природні з класами типів:

    class Ord a where
      (<) :: a -> a -> Bool
      ...
    
    min :: Ord a => a -> a -> a
    min x y = if x < y then x else y
    

    Тільки за допомогою підтипу Ordінтерфейс неможливо виразити. Вам потрібна більш складна, рекурсивна форма або параметричний поліморфізм під назвою "F-обмежене кількісне визначення", щоб зробити це точно. Порівняйте Java Comparableта його використання:

    interface Comparable<T> {
      int compareTo(T y);
    };
    
    <T extends Comparable<T>> T min(T x, T y) {
      if (x.compareTo(y) < 0)
        return x;
      else
        return y;
    }
    

З іншого боку, інтерфейси на основі підтипів природно дозволяють формувати неоднорідні колекції, наприклад, список типів List<C>може містити членів, які мають різні підтипи C(хоча відновити їх точний тип неможливо, за винятком випадків, коли використовуються знищення). Щоб зробити те саме на основі класів типів, вам потрібні екзистенційні типи як додаткова функція.


Ах, це має багато сенсу. Диспетчер на основі типу та вартості - це, мабуть, велика річ, про яку я не думав належним чином. Питання параметричного поліморфізму та більш конкретної типізації має сенс. Я щойно спіймав це та інтерфейси на основі підтипу разом у своєму розумі (мабуть, я думаю на Java: - /).
oconnor0

Чи існують екзистенційні типи чимось подібні до створення підтипів Cбез наявності зворотних збитків?
oconnor0

Типу. Вони є засобом для створення типу абстрактного, тобто приховування його подання. У Haskell, якщо ви також додаєте до нього обмеження класу, ви все одно можете використовувати методи цих класів, але нічого іншого. - Даунсинг - це фактично особливість, яка є окремою як від підтипу, так і від екзистенціальної кількісної оцінки, і, в принципі, може бути додана і в присутності останнього. Так само, як є мови OO, які не надають її.
Андреас Россберг

PS: FWIW, типові підстановки на Java - це екзистенційні типи, хоча і досить обмежені та спеціальні (що може бути частиною причини, чому вони дещо заплутані).
Андреас Россберг

1
@didierc, це обмежуватиметься випадками, які можна повністю вирішити статично. Більше того, для відповідності класам типів потрібна форма роздільної здатності перевантаження, яку можна розрізнити на основі лише типу повернення (див. Пункт 1).
Андреас Росберг

6

Окрім відмінної відповіді Андреаса, майте на увазі, що типи класів мають на меті упорядкувати перевантаження , що впливає на глобальний простір імен. У Haskell немає перевантаження, окрім тієї, яку можна отримати за допомогою класів типів. На противагу цьому, коли ви використовуєте об'єктні інтерфейси, про імена функцій у цьому інтерфейсі потрібно турбуватися лише тим функціям, які оголошені для аргументів цього інтерфейсу. Отже, інтерфейси забезпечують місцеві простори імен.

Наприклад, у вас був fmapоб’єктний інтерфейс під назвою "Functor". Було б цілком нормально мати інший fmapв іншому інтерфейсі, скажімо "Structor". Кожен об’єкт (або клас) може вибрати і вибрати, який інтерфейс він хоче реалізувати. Навпаки, у Haskell ви можете мати лише один fmapу конкретному контексті. Ви не можете імпортувати і класи типу Functor, і Structor в один контекст.

Об'єктні інтерфейси більше схожі на стандартні підписи ML, ніж на типи класів.


і все ж, здається, існує тісний взаємозв'язок між модулями ML та класами типу Haskell. cse.unsw.edu.au/~chak/papers/DHC07.html
Стівен Шоу

1

У вашому конкретному прикладі (з класом типу Functor) реалізації Haskell та Java поводяться по-різному. Уявіть, що у вас є тип даних, можливо, ви хочете, щоб це був Functor (це справді популярний тип даних в Haskell, який ви можете легко реалізувати і в Java). У вашому прикладі Java ви зробите, можливо, клас реалізує ваш інтерфейс Functor. Таким чином, ви можете написати наступне (просто псевдо-код, оскільки у мене є лише c # background):

Maybe<Int> val = new Maybe<Int>(5);
Functor<Int> res = val.fmap(someFunctionHere);

Зауважте, що resмає тип Functor, а не Можливо. Тому це робить Java реалізацією майже непридатною, оскільки ви втрачаєте конкретну інформацію про тип, і вам потрібно робити касти. (принаймні, мені не вдалося написати таку реалізацію, де типи все ще були). З класами типу Haskell ви отримаєте, можливо, Int.


Я думаю, що ця проблема пов'язана з тим, що Java не підтримує більш високі типи, а також не пов'язана з обговоренням інтерфейсів Vs типу. Якби Java мала більш високі типи, то fmap дуже добре міг би повернути a Maybe<Int>.
dcastro
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.