Объясните Классы Типа в Haskell - PullRequest
9 голосов
/ 21 апреля 2010

Я программист на C ++ / Java, и основной парадигмой, которую я использую в повседневном программировании, является ООП.В какой-то ветке я прочитал комментарий о том, что классы Type по своей природе более интуитивны, чем ООП.Может ли кто-нибудь объяснить концепцию классов типов простыми словами, чтобы такой парень из ООП, как я, мог это понять?

Ответы [ 6 ]

26 голосов
/ 21 апреля 2010

Во-первых, я всегда очень подозрительно отношусь к утверждениям о том, что та или иная структура программы более интуитивна. Программирование противоречит интуиции и всегда так будет, потому что люди, естественно, думают с точки зрения конкретных случаев, а не общих правил. Изменение этого требует обучения и практики, иначе известного как «обучение программированию».

Переходя к сути вопроса, ключевое отличие между классами OO и классами типов Haskell состоит в том, что в OO класс (даже класс интерфейса) является одновременно типом и шаблоном для новых типов (потомков). В Haskell класс типов является только шаблоном для новых типов. Точнее, класс типов описывает набор типов, которые имеют общий интерфейс, но сам по себе не является типом .

Таким образом, класс типов "Num" описывает числовые типы с операторами сложения, вычитания и умножения. Тип «Integer» является экземпляром «Num», что означает, что Integer является членом набора типов, которые реализуют эти операторы.

Так что я могу написать функцию суммирования с этим типом:

sum :: Num a => [a] -> a

Бит слева от оператора "=>" говорит, что "sum" будет работать для любого типа "a", который является экземпляром Num. Бит справа говорит о том, что он принимает список значений типа «а» и возвращает в результате одно значение типа «а». Таким образом, вы можете использовать его для суммирования списка целых чисел или списка двойных чисел или списка сложных, потому что все они являются экземплярами «Num». Реализация «sum» будет, конечно, использовать оператор «+», поэтому вам необходимо ограничение типа «Num».

Однако вы не можете написать это:

sum :: [Num] -> Num

потому что "Num" не тип.

Это различие между типом и классом типов является тем, почему мы не говорим о наследовании и потомках типов в Haskell. является своего рода наследованием для классов типов: вы можете объявить один класс типов как потомок другого. Потомок здесь описывает подмножество типов, описанных parent.

Важным следствием всего этого является то, что в Хаскеле не может быть разнородных списков. В примере «сумма» вы можете передать ему список целых чисел или список двойных чисел, но вы не можете смешивать двойные и целые числа в одном и том же списке. Это выглядит как хитрое ограничение; Как бы вы реализовали старый пример "автомобили и грузовики - оба типа транспортных средств"? Есть несколько ответов в зависимости от проблемы, которую вы на самом деле пытаетесь решить, но общий принцип заключается в том, что вы делаете свое косвенное обращение явно, используя первоклассные функции, а не неявно используя виртуальные функции.

12 голосов
/ 21 апреля 2010

Ну, короткая версия: Классы типов - это то, что Haskell использует для специального полиморфизма.

... но это, вероятно, ничего не прояснило для вас.

Полиморфизм должен быть знакомой концепцией для людей из ООП.Ключевым моментом здесь, однако, является разница между параметрическим и ad-hoc полиморфизмом.

Параметрический полиморфизм означает функции, которые работают с структурным типом, который сам параметризуется другими типами, такими как список значений.Параметрический полиморфизм является нормой повсюду в Хаскеле;C # и Java называют это "generics" .По сути, универсальная функция делает то же самое с определенной структурой, независимо от того, каковы параметры типа.

Специальный полиморфизм , с другой стороны, означает набор отдельных функций, делая разные (но концептуально связанные) вещи в зависимости от типов.В отличие от параметрического полиморфизма, специальные полиморфные функции необходимо указывать индивидуально для каждого возможного типа, с которым они могут использоваться. Специальный полиморфизм, таким образом, является обобщенным термином для множества функций, встречающихся в других языках, таких как перегрузка функций в C / C ++ или основанный на классах диспетчерский полиморфизм в ООП.

MajorПреимущество классов классов Хаскелла по сравнению с другими формами специального полиморфизма заключается в большей гибкости из-за , допускающего полиморфизм в любом месте сигнатуры типа .Например, большинство языков не различают перегруженные функции в зависимости от типа возвращаемого значения;классы типов могут.

Интерфейсы, которые встречаются во многих языках ООП, чем-то похожи на классы типов Хаскелла - вы указываете группу имен / сигнатур функций, которые вы хотите обрабатывать специальным полиморфным способом, а затем явноопишите, как различные типы могут использоваться с этими функциями.Классы типов в Haskell используются аналогичным образом, но с большей гибкостью: вы можете писать сигнатуры произвольных типов для функций класса типов, причем переменная типа, используемая для выбора экземпляра, будет выглядеть так: где угодно , как вам нравится, а не просто тип объекта , для которого вызываются методы.

Некоторые компиляторы Haskell, включая самый популярный, GHC, предлагают языковые расширения, которые делают классы типов еще более мощными, например multi-параметры типов параметров , которые позволяют вам выполнять специальную полиморфную диспетчеризацию функций на основе нескольких типов (аналогично тому, что называется «множественная диспетчеризация» в ООП).


Чтобы попытаться дать вам некоторое представление об этом, вот несколько смутно псевдокод с ароматом Java / C #:

interface IApplicative<>
{
    IApplicative<T> Pure<T>(T item);
    IApplicative<U> Map<T, U>(Function<T, U> mapFunc, IApplicative<T> source);
    IApplicative<U> Apply<T, U>(IApplicative<Function<T, U>> apFunc, IApplicative<T> source);
}

interface IReducible<>
{
    U Reduce<T,U>(Function<T, U, U> reduceFunc, U seed, IReducible<T> source);
}

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

10 голосов
/ 21 апреля 2010

В C ++ / etc "виртуальные методы" отправляются в соответствии с типом неявного аргумента this / self. (Метод указывается в таблице функций, на которую неявно указывает объект)

Классы типов работают по-разному и могут делать все, что могут "интерфейсы" и многое другое. Давайте начнем с простого примера того, что интерфейсы не могут сделать: тип класса Haskell Read.

ghci> -- this is a Haskell comment, like using "//" in C++
ghci> -- and ghci is an interactive Haskell shell
ghci> 3 + read "5" -- Haskell syntax is different, in C: 3 + read("5")
8
ghci> sum (read "[3, 5]") -- [3, 5] is a list containing 3 and 5
8
ghci> -- let us find out the type of "read"
ghci> :t read
read :: (Read a) => String -> a
Тип

read равен (Read a) => String -> a, что означает, что для каждого типа, реализующего класс Read, read может преобразовать String в этот тип. Это отправка, основанная на типе возврата, невозможная с «интерфейсами».

Этого нельзя сделать в подходе C ++ и др., Где таблица функций извлекается из объекта - здесь у вас даже нет соответствующего объекта до тех пор, пока read не вернет его, так как вы можете его вызвать?

Ключевое отличие реализации от интерфейсов, позволяющее это сделать, заключается в том, что таблица функций не указана внутри объекта, она передается компилятором отдельно вызываемым функциям.

Кроме того, в C ++ / etc, когда кто-то определяет класс, он также отвечает за реализацию своих интерфейсов. Это означает, что вы не можете просто изобрести новый интерфейс и заставить Int или std::vector реализовать его.

В Хаскеле вы можете, и это не "патчами обезьян", как в Руби. У Haskell есть хорошая схема размещения имен, которая означает, что два класса типов могут иметь функцию с одним и тем же именем, а тип все еще может реализовывать оба.

Это позволяет Haskell иметь много простых классов, таких как Eq (типы, которые поддерживают проверку на равенство), Show (типы, которые могут быть напечатаны в String), Read (типы, которые можно анализировать из String), Monoid (типы, у которых есть операция конкатенации и пустой элемент) и многое другое, и позволяет даже примитивным типам, таким как Int, реализовывать соответствующие классы типов.

Обладая богатством классов типов, люди, как правило, программируют на более общие типы, а затем имеют более многократно используемые функции, и, поскольку они имеют меньшую свободу, когда типы являются общими, они могут даже создавать меньше ошибок!

tldr: type-classes == потрясающе

7 голосов
/ 21 апреля 2010

В дополнение к тому, что xtofl и camccann уже написали в своих превосходных ответах, при сравнении интерфейсов Java с классами типов Haskell полезно отметить следующее:

  1. Java-интерфейсы закрыты , что означает, что набор интерфейсов, которые реализует любой данный класс, определяется раз и навсегда, когда и где он определен;

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

Эта открытость классов типов (и протоколов Clojure, которые очень похожи) является очень полезным свойством; для программиста на Haskell вполне обычно придумать новую абстракцию и немедленно применить ее к ряду проблем, связанных с уже существующими типами, посредством умного использования классов типов.

3 голосов
/ 21 апреля 2010

Класс типов можно сравнить с концепцией «реализации» интерфейса. Если какой-либо тип данных в Haskell реализует интерфейс «Показать», его можно использовать со всеми функциями, которые ожидают объект «Показать».

1 голос
/ 08 мая 2012

С ООП вы наследуете как интерфейс, так и реализацию. Классы типов Haskell позволяют разделить их. Два совершенно не связанных между собой типа могут предоставлять один и тот же интерфейс.

Возможно, еще важнее, что Haskell позволяет добавлять реализации классов "по факту". То есть я могу изобрести какой-то новый собственный класс типов, а затем пойти и сделать все стандартные предопределенные типы экземплярами этого класса. В ОО-языке вы [обычно] не можете легко добавить новый метод в существующий класс, независимо от того, насколько он будет полезен.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...