LINUX.ORG.RU

Typeclasses в Haskell

 ,


0

1

Часто можно услышать их сравнение с «интерфейсами в java» или что-то в таком духе. На мой взгляд, это плохая аналогия. Тайпклассы больше напоминают свободные шаблонные функции с неопределённой для общего типа реализацией (ну или [взаимно]определенные через другие функции, но это уже детали), которые надо явно инстанциировать для своего типа.

★★★

Последнее исправление: utf8nowhere (всего исправлений: 1)

с неопределённой для общего типа реализацией (ну или [взаимно]определенные через другие функции, но это уже детали)

class YoMomma a where
  yoMommaIsFat :: a -> IO ()
  yoMommaIsFat _ = print "YO MOMMA IS FAT!"

instance YoMomma Int

main = yoMommaIsFat (123 :: Int)
hateyoufeel ★★★★★
()
Ответ на: комментарий от hateyoufeel

Это ты типа показал контрпример?

которые надо явно инстанциировать для своего типа.

Что ты и доказал, лол. Только зачем?

utf8nowhere ★★★
() автор топика

Шаблоны в плюсах — это, по сути, макросы на стероидах. Реально тайпклассы — это просто типы данных, для которых нужное значение компилятор выводит сам.

Miguel ★★★★★
()
Ответ на: комментарий от xlsparse

Для каждого «обычные макросы» означает своё. Для сишников одно, для лисперов — другое.

utf8nowhere ★★★
() автор топика

напоминают свободные шаблонные функции с неопределённой для общего типа реализацией

какая гадость, нафига пытаться описать с точки зрения плюсов?, которые в этом аспекте слабее

лучше что-нибудь вроде: множество типов выраженное в форме «предиката наличия функций с заданной сигнатурой»; а когда, например «instance Ord a => ...» предикат выступает как ограничение

anonymous
()

ну, как бы я даже не знаю, в чём разница между «набором шаблонных функций с неопределённой для общего типа реализацией» и «интерфейсом java». разве интерфейс ява - это не набор шаблонных функций с неопределённой для общего типа реализацией? Учитывая что типы в интерфейсе могут быть дженериками то вообще всё на всё похоже. или нет?

AndreyKl ★★★★★
()
Ответ на: комментарий от Miguel

oo, я полуграмотен значит. спасибо, буду помалкивать.

AndreyKl ★★★★★
()
Ответ на: комментарий от AndreyKl

ну, как бы я даже не знаю, в чём разница между «набором шаблонных функций с неопределённой для общего типа реализацией» и «интерфейсом java».

В том, что для реализации интерфейса нужно наследовать свой тип от него. Т.е. создатель типа должен позаботиться обо всех интерфейсах, которые будет реализовывать тип.

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

utf8nowhere ★★★
() автор топика

В поддержку моей аналогии см. тут про то, как бы могла выглядешь реализация тайпкласса monoid в C++ (если бы там были концепты)

utf8nowhere ★★★
() автор топика
Ответ на: комментарий от utf8nowhere

Т.е. у нас есть тип который мы хотим определить. Пусть это будет тип А. И пусть так же есть функция hello принимающая значение типа int и возвращающая void.

Тогда положим для java у нас получается что то вроде

interface interfaceA {
  int hello(int i);
}

class A implements interfaceA {
  int hello(int i) {...}
}


Для хаскеля нужно изменить сигнатуру hello на что то вроде
hello :: InterfaceA -> Integer -> Integer


при этом сам InterfaceA нужно видимо определить как класс типов
typeclass InterfaceA where
  hello :: InterfaceA -> Integer -> Integer


тогда «наследующий» код выглядит так
instance InterfaceA A where
  hello a i = undefined


В общем-то я не понимаю аргументов.
По моему аналогичная ситуация. хочешь чтобы A реализовывало InterfaceA - объявляешь его инстансом и реализуешь все функции.

Или я не о том говорю?

UPD. Правда вот тут осознаю что кто из них шаблоны, кто дженерики, для меня тёмный лес. Т.е. дженерики - это штуки из явы, которые Array<A> array ? Или имеется ввиду GenericHaskell? И кто такие шаблоны? Всегда для себя считал что шаблоны с++ это как раз то что называется Generics в ява (и в общем-то использовал их именно так, правда практики немного). А тут чёрт разберёт что вы имеете ввиду все.

AndreyKl ★★★★★
()
Последнее исправление: AndreyKl (всего исправлений: 4)
Ответ на: комментарий от AndreyKl

При этом кажется что фраза «набор функций с неопределённой реализацией» в общем-то весьма неплохо описывает то что называют «интерфейс» в объектно-ориентированных языках.

Разве что слово «шаблонных» оттуда убрано, но в общем-то оно сути не меняет, только вместо int hello будем иметь Generic<A> A hello

AndreyKl ★★★★★
()
Ответ на: комментарий от Miguel

вот чтобы так сказать пояснить мне тёмному, прокомментируй, пжл

Typeclasses в Haskell (комментарий)

UPD. Правда вот тут осознаю что кто из них шаблоны, кто дженерики, для меня тёмный лес. Т.е. дженерики - это штуки из явы, которые Array<A> array ? Или имеется ввиду GenericHaskell? И кто такие шаблоны? Всегда для себя считал что шаблоны с++ это как раз то что называется Generics в ява (и в общем-то использовал их именно так, правда практики немного). А тут чёрт разберёт что вы имеете ввиду все.

AndreyKl ★★★★★
()
Ответ на: комментарий от AndreyKl

Т.е. дженерики - это штуки из явы, которые Array<A> array ?

Ага.

Всегда для себя считал что шаблоны с++ это как раз то что называется Generics в ява (и в общем-то использовал их именно так, правда практики немного).

А вот это — неправда. Хотя соглашусь, области их использования во многом пересекаются.

Фишка вот в чём. Дженерик (в жабе, скале, окамле, хаскеле и прочих нормальных языках) — это одна функция, которая готова работать с разными типами. Она может быть скомпилирована отдельно, и после этого принимать ЛЮБОЙ тип, который соответствует заданным — при её создании — требованиям. То есть, компилятор, конечно, вправе сделать особые, оптимизированные её версии для разных типов (и, скажем, GHC так и делает), но для программиста это всё прозрачно: одна функция. А самое важное — семантика этой функции определена в терминах производимых ею действий.

Шаблон же — это такой макрос. Если ты используешь его с новым типом, то где-то за занавесом создаётся новая функция, специально для работы с этим типом. И семантика шаблона определяется в терминах того КОДА, который он генерирует. Код при этом может лишь внешне выглядеть похоже, а работать совсем по-другому.

Отсюда сильные и слабые стороны тех и других.

1) Шаблон невозможно скомпилировать отдельно. Весь код шаблона должен быть доступен всякий раз, когда он используется. Иначе невозможно будет по этому образцу построить код настоящей функции. Именно поэтому вся реализация шаблонов пихается в .h-файлы. Теоретически, есть такая вещь как «раздельная компиляция шаблонов» (в некоторых компиляторах поддерживается); практически её нет, и весь исходный код шаблона всё равно должен быть доступен.

2) Шаблоны не поддерживают полиморфную рекурсию. Допустим, для того, чтобы вычислить f<Type>(x), тебе нужно ПОРОЙ (не всегда) вычислять f<X<Type>>(...). С дженериками проблемы нет, если только ты можешь гарантировать, что X<Type> удовлетворяет всем необходимым ограничениям — функция-то одна. А вот шаблон, попытавшись сгенерировать код для f<Type>, наткнётся на необходимость сгенерировать код для f<X<Type>> (неважно, что он не всегда используется, код-то генерируется не в рантайме), а для этого нужно сгенерировать код для f<X<X<Type>>>... и мы упираемся в переполнение стека на этапе компиляции.

3) Дженерики не поддерживают частичную специализацию. Функция должна быть одна, и ты не можешь вдруг сказать «а, для типа int мы напишем другую реализацию, получше». То есть, можешь — но тебе нужно предусмотреть это изначально, при создании дженерика. Когда дженерик скомпилирован — всё, менять в нём уже ничего нельзя.

4) Сообщения об ошибках. Дженерик проверяет корректность кода сразу. Шаблон проверяет только синтаксис. О том, что, оказывается, требуется, чтобы тип-аргумент шаблона поддерживал какой-то специфический метод, ты узнаешь только тогда, когда попытаешься использовать шаблон — или когда прочитаешь весь его код. С дженериком достаточно знать его заголовок, там — все необходимые требования. И о том, что дженерик написан некорректно, ты узнаешь сразу, как только попытаешься скомпилировать файл, в котором он определён.

Особняком стоит Rust — в нём взяли худшее их обоих миров. Реально там шаблоны (они решают проблему (1), запихивая исходный код в скомпилированный бинарник), и полиморфной рекурсии там нет; но и частичной специализации тоже нет (без понятия, почему).

Т.е. создатель типа должен позаботиться обо всех интерфейсах, которые будет реализовывать тип.

Ну, это жабопроблемы. В хаскеле, например, вполне можно реализовать новый интерфейс уже после того, как создан тип. А Скала более-менее имитирует такую возможность, позволяя создавать собственные неявные преобразования, из одного типа в другой.

Miguel ★★★★★
()
Последнее исправление: Miguel (всего исправлений: 1)
Ответ на: комментарий от Miguel

Искренне надеюсь, что ты не прав про rust:

там шаблоны (они решают проблему (1), запихивая исходный код в скомпилированный бинарник)

Как в этом можно убедиться?

nezamudich ★★
()
Ответ на: комментарий от Miguel

Хотя нет, вижу, что ты прав. https://doc.rust-lang.org/0.11.0/tutorial.html#generics

The Rust compiler compiles generic functions very efficiently by monomorphizing them. Monomorphization is a fancy name for a simple idea: generate a separate copy of each generic function at each call site, a copy that is specialized to the argument types and can thus be optimized specifically for them. In this respect, Rust's generics have similar performance characteristics to C++ templates.

nezamudich ★★
()
Ответ на: комментарий от nezamudich

Искренне надеюсь, что ты не прав про rust:

Ну, если быть совсем точным, то в бинарник пихается AST, а не сам код. Но, ИМХО, разницы особой нет.

Как в этом можно убедиться?

Ух. Не помню. Я оригинально нашёл этот ответ здесь: https://www.reddit.com/r/rust/comments/2c6pn0/how_does_rust_implement_calling...

Логически, да, другого способа сделать эту мономорфизацию, в общем-то, и нет.

Miguel ★★★★★
()
Ответ на: комментарий от nezamudich

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

anonymous
()
Ответ на: комментарий от anonymous

Это просто оптимизация, которая не влияет на семантику.

Как скажешь.

Для примера решил загуглить finger tree на rust. Среди прочих встречаются очень милые реализации, типа этой: https://gist.github.com/andrebeat/4d5434e94b5d43ed9931

// error: the type `fingers::Digit<fingers::Node<fingers::Node<fingers::Node<fingers::Node<fingers::Node<fingers::Node<fingers::Node<fingers::Node<fingers::Node<fingers::Node<fingers::Node<fingers::Node<fingers::Node<fingers::Node<fingers::Node<fingers::Node<fingers::Node<fingers::Node<fingers::Node<fingers::Node<fingers::Node<fingers::Node<fingers::Node<fingers::Node<fingers::Node<fingers::Node<fingers::Node<usize>>>>>>>>>>>>>>>>>>>>>>>>>>>>` is too big for the current architecture

nezamudich ★★
()
Последнее исправление: nezamudich (всего исправлений: 1)
Ответ на: комментарий от AndreyKl

Для хаскеля нужно изменить сигнатуру hello

Для хаскеля ничего менять не нужно.

utf8nowhere ★★★
() автор топика
Ответ на: комментарий от Miguel

Где я неправ?

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

Ну и вот это:

Дженерик проверяет корректность кода сразу. Шаблон проверяет только синтаксис.

В Rust, где, по твоему мнению, шаблоны, а не дженерики, корректность проверяется сразу.

tailgunner ★★★★★
()
Ответ на: комментарий от tailgunner

Мономорфизация - это просто деталь реализации

Это, к сожалению, деталь реализации, которая протекает. GHC, как я уже упоминал, тоже делает частичную мономорфизацию, генерируя код, специализированный под всё, что ему известно. Но программист этого не видит, по крайней мере, пока не начинает изучать потребление ресурсов.

В Rust, где, по твоему мнению, шаблоны, а не дженерики, корректность проверяется сразу.

Как я, опять-таки, уже говорил, Rust стоит особняком.

Miguel ★★★★★
()
Ответ на: комментарий от tailgunner

В Rust, где, по твоему мнению, шаблоны, а не дженерики, корректность проверяется сразу.

Проверил, и правда проверяется. Только в чём профит, я не понимаю. Максимум, что я могу в функции с дженерик-аргументом это передать его в другую дженерик-функцию (или ничего не делать с ним).

anonymous
()
Ответ на: комментарий от Miguel

Как я, опять-таки, уже говорил, Rust стоит особняком.

Да. Но, как ты сказал:

Miguel> Реально там шаблоны

А там дженерики.

И кстати, в Аде тоже дженерики. С мономорфизацией. Гугл говорит, что и в MLton то же самое, так что, я думаю, обсуждение «то, что с мономорфизацией - не дженерики» можно закрывать.

Ну а родовые травмы Си++, где реализация шаблонов выросла из препроцессора - это всего лишь травмы.

tailgunner ★★★★★
()
Ответ на: комментарий от anonymous

Только в чём профит, я не понимаю

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

tailgunner ★★★★★
()
Ответ на: комментарий от tailgunner

А. Для ограничений на дженерик-тайп там трейты завезли. Ну ок.

anonymous
()
Ответ на: комментарий от tailgunner

А там дженерики.

По какому признаку?

Свой критерий я озвучил: генерация кода заново каждый раз из исходника (OK, из AST, но разницы почти нет). Озвучишь свой?

Miguel ★★★★★
()
Ответ на: комментарий от tailgunner

По параметрическому полиморфизму.

Его там не больше, чем в тех же плюсах. То есть, по-правде — вообще нет.

Настоящий параметрический полиморфизм будет там, где допустима полиморфная рекурсия.

Miguel ★★★★★
()
Ответ на: комментарий от Miguel

Настоящий параметрический полиморфизм будет там, где допустима полиморфная рекурсия.

Вот, начинаем подбираться к сути. Итак, настоящие дженерики - это дженерики с полиморфной рекурсией или как?

tailgunner ★★★★★
()
Ответ на: комментарий от nezamudich

Для примера решил загуглить finger tree на rust. Среди прочих встречаются очень милые реализации, типа этой:

Проблема не в монорфизации. Еще раз, GHC ее тоже _делает_. И это не мешает велосипедить finger trees на хаскеле.

anonymous
()
Ответ на: комментарий от Miguel

Настоящие дженерики — это дженерики с полноценной раздельной компиляцией.

Единственный язык в котором есть такие генерики - это шарп (т.к. на уровне рантайма прям есть такой объект - «генерик»). И все. GHC, например, мимо.

anonymous
()
Ответ на: комментарий от Miguel

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

anonymous
()
Ответ на: комментарий от Miguel

Настоящие дженерики — это дженерики с полноценной раздельной компиляцией.

Осталось только определить критерий полноценности. Чем сериализованный AST не устроил?

tailgunner ★★★★★
()
Ответ на: комментарий от tailgunner

Осталось только определить критерий полноценности.

Мы, слава богу, не в суде.

Чем сериализованный AST не устроил?

А чем он отличается от .h-файлов?

Miguel ★★★★★
()
Ответ на: комментарий от Miguel

Чем сериализованный AST не устроил?

А чем он отличается от .h-файлов?

Даже не знаю, с чего начать... гарантированным отсутствием синтаксических ошибок? Отсутствием фазы трансляции? Независимостью от опций компиляции? Гарантированным отсутствием ошибок типов (в случае Rust)?

Так чем не устроил?

tailgunner ★★★★★
()
Ответ на: комментарий от Miguel

По какому признаку?

Свой критерий я озвучил: генерация кода заново каждый раз из исходника (OK, из AST, но разницы почти нет). Озвучишь свой?

Мне кажется главный критерий - это параметрический полиморфизм. Это можно свести к вопросу о том, является ли сам дженерик SomeType<_> типом(параметризованным) или же типом являются его конкретные инстансы. Отсюда следует и отсутствие для дженерика возможности специализации для конкретного типа(т.к. это уже ad-hoc полиморфизм, а не параметрический). Так же из этого следует и возможность задания ковариантности/контрвариантности параметра.

Например, в C++ std::vector - это не тип, это шаблон типа, т.е. типы будут сгенерированы позднее, по заданному шаблону.

anonymous
()
Ответ на: комментарий от tailgunner

гарантированным отсутствием синтаксических ошибок?

Пфуй. Синтаксис — это совершеннейшая мелочь. Даже на интервью не придираются.

Независимостью от опций компиляции?

Это как раз не гарантировано для одного лишь сериализованного AST.

Так чем не устроил?

Да тем самым. Преобразование Source->AST — это микроскопическая часть компиляции.

Miguel ★★★★★
()
Ответ на: комментарий от Miguel

Это как раз не гарантировано для одного лишь сериализованного AST.

Практические примеры есть?

Так чем не устроил?

Да тем самым.

Да конкретнее.

Преобразование Source->AST — это микроскопическая часть компиляции.

Я назвал еще как минимум одну часть - проверку типов. Если и она для тебя микроскопическая - ну окей, ты снова имеешь право на свое особое мнение.

tailgunner ★★★★★
()
Ответ на: комментарий от tailgunner

Практические примеры есть?

Опции оптимизации у Rust есть? Если да — то они обязаны влиять на компиляцию этого AST.

проверку типов

Проверка типов, хоть и очень хороша сама по себе, для корректной программы на результат не влияет.

Нет, я не буду спорить, что это отметает пункт (4) выше. Но раздельная компиляция от этого не появится.

Ну, представь, что вышла новая версия библиотеки. С какими-нибудь исправленными багами, но совместимая по интерфейсу. А у тебя клиент уже скомпилирован. Тебе достаточно линковки или нужно перекомпилировать клиент полностью?

Miguel ★★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.