Подтвердить что ты не робот

Разница между интерфейсами ООП и классами FP

Возможный дубликат:
Интерфейс Java и класс типа Haskell: различия и сходства?

Когда я начал изучать haskell, мне сказали, что классы типов более мощные, чем/разные для интерфейсов.

Через год я широко использовал интерфейсы и типы таблиц, и мне еще предстоит увидеть пример или объяснение того, как они отличаются. Это либо не откровение, которое приходит естественным образом, я пропустил что-то очевидное, или на самом деле нет никакой реальной разницы.

В интернете нет ничего существенного. Итак, у вас есть ответ?

4b9b3361

Ответ 1

Вы можете смотреть на это с разных ракурсов. Другие люди не согласятся, но я думаю, что интерфейсы ООП - это хорошее место для начала, чтобы понять классы классов (конечно, по сравнению с тем, что они начинаются ни с чем).

Людям нравится указывать, что концептуально классы типов классифицируют типы, подобно наборам, - "набор типов, которые поддерживают эти операции, а также другие ожидания, которые не могут быть закодированы в самом языке". Это имеет смысл и иногда делается для объявления класса типа без методов, говоря "только сделайте свой тип экземпляром этого класса, если он отвечает определенным требованиям". Это происходит очень редко с интерфейсами ООП.

В терминах конкретных различий существует несколько способов, в которых классы классов более мощные, чем интерфейсы ООП:

  • Самый большой из них - это то, что typeclasses отделяют объявление от того, что тип реализует интерфейс из объявления самого типа. С интерфейсами OOP вы указываете интерфейсы, которые реализует тип, когда вы его определяете, и нет возможности добавить более позднее. С классами типов, если вы создаете новый тип, который может быть реализован, но не знает о данном типе "вверх по иерархии модулей", вы можете написать объявление экземпляра. Если у вас есть тип и класс типа от третьих сторон, которые не знают друг о друге, вы можете написать объявление экземпляра. В аналогичных случаях с интерфейсами OOP вы в основном просто застреваете, хотя языки OOP разработали "шаблоны проектирования" (адаптер), чтобы обойти ограничения.

  • Следующим по величине (это субъективно, конечно) является то, что, хотя концептуально интерфейсы ООП представляют собой кучу методов, которые могут быть вызваны для объектов, реализующих интерфейс, типы классов - это куча методов, которые могут быть используется с типами, которые являются членами класса. Различие важно. Поскольку методы класса класса определены со ссылкой на тип, а не объект, нет препятствий для использования методов с несколькими объектами типа в качестве параметров (операторов равенства и сравнения) или которые возвращают объект типа в результате ( различные арифметические операции) или даже константы типа (минимальная и максимальная границы). Интерфейсы OOP просто не могут этого сделать, а языки ООП разработали шаблоны проектирования (например, метод виртуального клонирования), чтобы обойти ограничение.

  • Интерфейсы OOP могут быть определены только для типов; классы типов также могут быть определены для так называемых "конструкторов типов". Различные типы коллекций, определенные с использованием шаблонов и дженериков на разных языках OOP, основанных на C, являются конструкторами типов: List принимает тип T как аргумент и строит тип List <T> . Классы типов позволяют объявлять интерфейсы для конструкторов типов: скажем, операцию сопоставления для типов коллекций, которая вызывает предоставленную функцию для каждого элемента коллекции, и собирает результаты в новой копии коллекции - потенциально с другим типом элемента! Опять же, вы не можете сделать это с интерфейсами OOP.

  • Если заданный параметр должен реализовывать несколько интерфейсов, с классами классов тривиально легко перечислять, из каких из них он должен быть членом; с интерфейсами OOP, вы можете указать только один интерфейс как тип заданного указателя или ссылки. Если вам нужно, чтобы реализовать больше, ваши единственные опции - непривлекательные, такие как запись одного интерфейса в подписи и кастинг другим, или добавление отдельных параметров для каждого интерфейса и требование, чтобы они указывали на один и тот же объект. Вы даже не можете разрешить это, объявив новый пустой интерфейс, который наследуется от тех, которые вам нужны, потому что тип не будет автоматически рассматриваться как реализация вашего нового интерфейса только потому, что он реализует своих предков. (Если вы можете объявить реализации после факта, это не будет такой проблемой, но да, вы тоже не можете этого сделать.)

  • Относительно обратного случая выше, вы можете потребовать, чтобы два параметра имели типы, которые реализуют конкретный интерфейс и что они одного типа. С интерфейсами OOP вы можете указать только первую часть.

  • Объявления экземпляров классов типов более гибкие. С интерфейсами OOP вы можете только сказать: "Я объявляю тип X, и он реализует интерфейс Y", где X и Y являются конкретными. С типами классов вы можете сказать "все типы списков, типы элементов которых удовлетворяют этим условиям, являются членами Y". (Вы также можете сказать: "все типы, которые являются членами X и Y, также являются членами Z", хотя в Haskell это проблематично по ряду причин.)

  • Так называемые "ограничения суперкласса" более гибкие, чем просто наследование интерфейса. С интерфейсами OOP вы можете только сказать "для типа для реализации этого интерфейса, он также должен реализовать эти другие интерфейсы". То, что наиболее распространенный случай с типами классов, а также ограничения суперкласса, также позволяют говорить такие вещи, как "SomeTypeConstructor <ThisType> должен реализовывать так называемый интерфейс" или "результаты этого типа, применяемые к типу, and-so constraint" и т.д.

  • В настоящее время это языковое расширение в Haskell (как и функции типа), но вы можете объявлять классы типов, включающие несколько типов. Например, класс изоморфизма: класс пар типов, где вы можете без потерь конвертировать из одного в другое и обратно. Опять же, невозможно использовать интерфейсы ООП.

  • Я уверен, что там больше.

Стоит отметить, что в языках ООП, которые добавляют дженерики, некоторые из этих ограничений можно стереть (четвертая, пятая, возможно, вторая точка).

С другой стороны, есть две существенные вещи, которые могут выполнять интерфейсы ООП и набирать классы изначально:

  • Динамическая отправка времени выполнения. В языках ООП тривиально обходить и хранить указатели на объект, реализующий интерфейс, и вызывать методы на нем во время выполнения, которые будут разрешены в соответствии с динамическим типом времени выполнения объекта. Напротив, ограничения типа класса по умолчанию определяются во время компиляции - и, возможно, удивительно, что в подавляющем большинстве случаев это все, что вам нужно. Если вам нужна динамическая отправка, вы можете использовать так называемые экзистенциальные типы (которые в настоящее время являются языковым расширением в Haskell): конструкция, в которой он "забывает", что такое тип объекта, и только помнит (по вашему усмотрению), что он подчинялся определенным ограничениям типа класса. С этого момента он ведет себя в основном точно так же, как указатели или ссылки на объекты, реализующие интерфейсы на языках ООП, а типы классов не имеют дефицита в этой области. (Следует отметить, что если у вас есть два экзистенциальных объекта, реализующих один и тот же тип класса, и метод класса типа, который требует двух параметров его типа, вы не можете использовать экзистенции в качестве параметров, потому что вы не можете знать, экзистенты имели один и тот же тип. Но по сравнению с языками ООП, которые не могут иметь такие методы в первую очередь, это не потеря.)

  • Время выполнения объектов для интерфейсов. В языках ООП вы можете взять указатель или ссылку во время выполнения и проверить, реализует ли он интерфейс, и "отбрасывает" его на этот интерфейс, если это так. Типы классов не имеют ничего эквивалентного (что в некоторых отношениях является преимуществом, поскольку оно сохраняет свойство, называемое "параметричность", но я не буду вдаваться в это здесь). Конечно, нет ничего, что помешало бы вам добавить новый класс типа (или расширить существующий) с помощью методов, чтобы подвергать объекты типа экзистенции того типа, который вы хотите. (Вы также можете реализовать такую ​​возможность более универсально, как библиотеку, но ее значительно больше задействовать. Я планирую закончить ее и выгрузить ее в Hackage когда-нибудь, я обещаю!)

(я должен указать, что, хотя вы * можете * делать эти вещи, многие считают, что имитирует OOP таким образом плохой стиль и предлагает вам использовать более простые решения, такие как явные записи функций вместо классов типов. класс, эта опция не менее мощная.)

Оперативно интерфейсы OOP обычно реализуются путем хранения указателя или указателей в самом объекте, которые указывают на таблицы указателей функций для интерфейсов, которые реализует объект. Классы типов обычно реализуются (для языков, которые выполняют полиморфизм по боксу, например Haskell, а не полиморфизм по-нескольким попыткам, например С++) путем "передачи словаря": компилятор неявно передает указатель на таблицу функций (и константы ) в качестве скрытого параметра для каждой функции, которая использует класс типа, и функция получает одну копию независимо от того, сколько объектов задействовано (именно поэтому вы можете делать то, о чем упоминалось во втором пункте выше). Реализация экзистенциальных типов очень похожа на то, что делают языки ООП: указатель на словарь типа-типа хранится вместе с объектом как "доказательство", что его "забытый" тип является его членом.

Если вы когда-либо читали о предложении "концепций" для С++ (как это было первоначально предложено для С++ 11), то в основном классы типа Haskell переопределяются для шаблонов С++. Я иногда думаю, что было бы неплохо иметь язык, который просто берет С++ - с-понятиями, вырывает из него объектно-ориентированную и виртуальную функции, очищает синтаксис и другие бородавки и добавляет экзистенциальные типы, когда вам нужно время выполнения динамическая отправка по типу. (Обновление: Rust в основном это, со многими другими приятными вещами.)

Ответ 2

Я предполагаю, что вы говорите о типах классов Haskell. На самом деле это не разница между интерфейсами и классами классов. Как указано в названии, класс типа - это просто класс типов с общим набором функций (и связанных типов, если вы включаете расширение TypeFamilies).

Однако система типа Haskell сама по себе более эффективна, чем, например, система типа С#. Это позволяет вам писать классы типов в Haskell, которые вы не можете выразить в С#. Даже класс типа, такой же простой, как Functor, не может быть выражен в С#:

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

Проблема с С# заключается в том, что дженерики не могут быть общими. Другими словами, в С# только типы вида * могут быть полиморфными. Haskell допускает конструкторы полиморфного типа, поэтому типы любого типа могут быть полиморфными.

Вот почему многие из мощных общих функций в Haskell (mapM, liftA2 и т.д.) не могут быть выражены на большинстве языков с менее мощной системой типов.

Ответ 4

Основное отличие - что делает классы классов более гибкими, чем интерфейсы, - это то, что типы-классы не зависят от его типов данных и могут быть добавлены впоследствии. Еще одно отличие (по крайней мере, от Java) заключается в том, что вы можете предоставить стандартные реализации. Пример:

//Java
public interface HasSize {
   public int size();
   public boolean isEmpty();
}

Наличие этого интерфейса хорошо, но нет возможности добавить его в существующий класс, не меняя его. Если вам повезет, класс не является окончательным (скажем ArrayList), поэтому вы можете написать подкласс, реализующий интерфейс для него. Если класс является окончательным (скажем String), вам не повезло.

Сравните это с Haskell. Вы можете написать тип-тип:

--Haskell
class HasSize a where
  size :: a -> Int
  isEmpty :: a -> Bool
  isEmpty x = size x == 0

И вы можете добавить существующие типы данных в класс, не касаясь их:

instance HasSize [a] where
   size = length

Другим приятным свойством типов-классов является неявный вызов. Например. если у вас есть Comparator в Java, вам нужно передать его как явное значение. В Haskell эквивалент Ord может использоваться автоматически, как только соответствующий экземпляр находится в области видимости.