Ответ 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 в основном это, со многими другими приятными вещами.)