Здравствуйте, x-code, Вы писали:
XC>Читаю статью XC>http://habrahabr.ru/post/239151/ XC>так и не могу в точности понять, что же такое "класс типов". Можете объяснить что это за концепция?
Если грубо, то это как generic-интерфейсы, только конкретная реализация задаётся не в определении типа, а вне его.
Здравствуйте, x-code, Вы писали:
XC>Читаю статью XC>http://habrahabr.ru/post/239151/ XC>так и не могу в точности понять, что же такое "класс типов". Можете объяснить что это за концепция?
Если брать C++ STL — там есть такие концепции как Forward iterator, Random access iterator, Container, Associative container и т.п.
Для этих концепций есть определённые синтаксические и семантические требования. Например для Container, одно из требований это метод .size(), который должен возвращать значение типа size_type. Концепция это аналог "класса типов".
Типы которые удовлетворяют той или иной концепции, называются моделями соответствующей концепции. Например vector<double> это модель концепции Container, std::set<int> — это тоже модель концепции Container, при этом у них нет общих предков в ООП смысле.
Концепция — это своего рода аналог интерфейсов из ООП, только более обобщённый. Концепции могут задавать соотношения между типами в сигнатурах (например T operator+(T, T), то есть типы параметров и результата должны быть одинаковы), а не только жёстко прибивать конкретные типы (как это делают абстрактные классы из ООП — virtual int operator+(int, int)). Концепции не ограниченны описанием требований к членам моделей — например, они могут определять требования и к non-member functions.
Аналогично тому как в ООП один интерфейс может наследовать несколько других — одна концепция может являться уточнением более общих концепций. Например концепции Associative container и Sequence container — это уточнения концепции Container, они добавляют дополнительные требования к моделям.
Здравствуйте, x-code, Вы писали:
XC>Читаю статью XC>http://habrahabr.ru/post/239151/ XC>так и не могу в точности понять, что же такое "класс типов". Можете объяснить что это за концепция?
Интерфейсы, реализация которых задается вне класса.
Таким образом, класс C можно передать в метод, требующий интерфейса I, если мы напишем в текущей области видимости реализацию I для C.
Если в текущей области видимости больше чем одна реализация I для C, то возникает конфликт, который надо разруливать.
Здравствуйте, x-code, Вы писали:
XC>Здравствуйте, Evgeny.Panasyuk, Вы писали:
XC>Спасибо, стало значительно понятнее! Кстати, это как-то пересекается с понятием структурной типизации?
Здравствуйте, x-code, Вы писали:
XC>Читаю статью XC>http://habrahabr.ru/post/239151/ XC>так и не могу в точности понять, что же такое "класс типов". Можете объяснить что это за концепция?
Это контракт типа. Или интерфейс типа, в широком смысле.
Здравствуйте, AlexRK, Вы писали:
ARK>Интерфейсы, реализация которых задается вне класса. ARK>Таким образом, класс C можно передать в метод, требующий интерфейса I, если мы напишем в текущей области видимости реализацию I для C. ARK>Если в текущей области видимости больше чем одна реализация I для C, то возникает конфликт, который надо разруливать.
Нет, классы типов — это не интерфейсы. Между интерфейсами и имплементирующими их классами существует отношение подтипирования. Между тайпклассом и типом, для которого определен его инстанс такого отношения нет. Т.е. скастить к тип к тайпклассу нельзя.
'You may call it "nonsense" if you like, but I'VE heard nonsense, compared with which that would be as sensible as a dictionary!' (c) Lewis Carroll
Здравствуйте, Klapaucius, Вы писали:
K>Нет, классы типов — это не интерфейсы. Между интерфейсами и имплементирующими их классами существует отношение подтипирования.
С чего вы взяли? Это особенность реализации.
В Java так и пишут — "implements", а наследование делается через другой синтаксис.
K>Между тайпклассом и типом, для которого определен его инстанс такого отношения нет. Т.е. скастить к тип к тайпклассу нельзя.
Ну так есть экзистенциальные типы, которые по сути есть то же самое, вид сбоку.
Для начала нужно сказать, что тайпклассами сейчас называют все что угодно, что может запутать кого угодно. В статье, на которую вы ссылаетесь упоминается Go, но говорить о классах типов в языках без параметрического полиморфизма просто бессмысленно.
В языках с параметрическим полиморфизмом значение параметризованного типа огорожено непробиваемым барьером абстракции. Параметр a означает "для любого типа", а над любым типом могут быть заданы или не заданы разные операции, могут быть не определены вообще никакие, минимального обязательного набора нет. Да, в некоторых языках, вроде C# квантификация всегда ограничена по крайней мере интерфейсом класса object, т.е. сделать можно много чего, но в нормальном параметрическом полиморфизме мы можем только куда-то помещать, передавать, или вовсе игнорировать значение, поэтому число осмысленных реализаций полиморфных функций может быть сильно ограничено.
На этом этапе думаю уже понятно, что рассуждающие про сиплюсплюсные темплейты и концепты по соседству не понимают о чем говорят, ничего общего все это с параметрическим полиморфизмом и классами типов не имеет.
Развеяв некоторые недоразумения, переходим к примерам.
Попробуем определить функцию, суммирующую элементы некоего контейнера, для которого свертка определены.
sum :: [a] -> a
sum = fold (+) 0
стоп, так не получится. Ни плюс, ни 0 использовать нельзя, это не C++, где операции могут быть задокументированы в виде концепта, но вообще это не требуется — тут параметрический полиморфизм. Чтоб что-то там суммировать, придется передать в функцию подходящие другие функции:
sum :: (a -> a -> a) -> a -> [a] -> a
sum (+) 0 = fold (+) 0
Протягивать все эти функции через параметры по одной — удовольствие сомнительное. К тому же, операции — в данном случае (+) и ноль — обычно сгруппированы не просто так, у этой группы операций есть свойства.
Для плюса и ноля это
0 + x = x
x + 0 = x
т.е. 0 — нейтральный элемент
(это рекомендуемое правило для разбиения операций на такие группы — у группы операций должны быть какие-то нетривиальные свойства)
Будем хранить и передавать такие операции вместе.
Определим для этой пары тип
data Add a = Add { plus :: a -> a -> a, zero :: a }
и перепишем функцию соотв. образом:
sum :: Add a -> [a] -> a
sum (Add (+) 0) = fold (+) 0
Определим готовые группы операций (словари) для типов и положим в библиотеку.
На данном этапе у нас две проблемы:
1) Мы должны расставлять словари руками.
2) Мы можем ошибиться и разные части вычисления (сортировка, например) будут сделаны с разными словарями и ответ получится неверным.
Эти проблемы могут показаться после такого примера надуманными, но это вовсе не так: в реальности словари могут образовывать причудливые и развесистые структуры, собирать их руками без ошибок — это точно не то, чем мечтает заняться программист.
Тайплклассы задуманы так, чтоб решить обе эти проблемы.
class Add a where
plus :: a -> a -> a
zero :: a
instance Add Int where
plus = (+#)
zero = 0#
instance Add Double where
plus = (+##)
zero = 0.0#
Рассахаривается именно в описанные выше значения-словари.
Сигнатура функции sum меняется таким образом:
sum :: Add a => [a] -> a
Для классов типов (да — это уже они) перечисленных слева от двойной стрелки => компилятор найдет словари и передаст их в функцию через неявный параметр.
В данном случае, как мне кажется, узнавание деталей реализации способствует формированию интуитивного представления о том, что такое классы типов и как они работают.
Можно сказать, что многое из того, что вы ожидаете от такой модели, вроде:
"Наследования" классов, в том числе и множественного (включения другого словаря (словарей) в качестве поля (полей) в словарь)
Реализации функций из словаря в терминах друг друга и функций из словарей, "наследником" которых этот словарь является
И даже тех, с которыми он может опционально оказаться в одном "пакете" словарей для передачи через параметр
Того, что у типа-словаря может быть несколько параметров типов
Того, что словари могут сами быть полиморфными, инстансы могут быть объявлены для других классов типов — не обязательно для какого-то конкретного типа.
Того, что можно объявлять синонимы для типов-словарей т.е. классов типов и даже групп таких классов.
и т.д. можно ожидать и от классов. Чего нельзя ожидать — так это свободы в выборе передаваемого словаря, потому что это несовместимо с решением проблемы 2. Особо нужно отметить, что бывают автоматически генерируемые словари-инстансы, которые никто нигде в коде вручную не описывает.
Помимо этого способа реализации, называемого "передача словаря", существуют и другие, например с помощью GADT, как в JHC http://repetae.net/computer/jhc/jhc-reify-typeclass.html , однако это довольно таки обходной путь объяснения классов типов. Кроме того, классы типов могут быть и вовсе фиктивными, не имеющими соответствующих передаваемых куда-то словарей функций.
Никакой структурной типизации, про которую тут по соседству говорили нет: тип, структурно эквивалентный типу с другим именем, для которого инстанс класса есть — не имеет инстанса этого класса, пока тот не будет определен явно, типизация тут номинативная. Правда, в случае "оберток", например, инстанс может быть выведен, но только после явного указания. Т.е. если без имплементации инстанса можно обойтись, то без декларации о том, что существует инстанс класса С для типа T — нет.
Обратите внимание на то, что имплементации существуют отдельно от значений типов. Они могут быть определены отдельно, они передаются в функции отдельно. Нет необходимости, например, хранить в каждом значении типа Int ссылку на словарь addInt. (хотя возможность такая есть, если нужна диспетчеризация в рантайме). Каждому типу соотвествует один инстанс класса, поэтому специального связывания данных с функцими не требуется.
На практике можно считать, что GHC "гарантирует" глобальную уникальность инстансов (проверки можно обойти несколькими способами, но они в достаточной степени хороши для того, чтоб на них можно было рассчитывать).
Первая проблема также решается в языках c неявными параметрами:
sum :: {Add a} -> [a] -> a
Параметр в фигурных скобках — неявный, компилятор попытается найти в области видимости подходящий словарь и подставит его. Никаких попыток поддержать глобальную уникальность инстансов тут не делается.
Проблема 2 в таких языках не решается даже на уровне, на котором она решается в GHC, т.е. рассчитывать на уникальность нельзя и простой перенос хаскельных практик работы с тайпклассами в эти языки из хаскеля невозможен. Хотя про такие языки обычно говорят, что тайпклассы в них есть, следует учитывать, что это наличие "с оговорками".
В таких языках модель передачи словаря можно воспринимать без всяких оговорок, так как никаких ограничений на ручной выбор передаваемого словаря они не накладывают.
Здравствуйте, AlexRK, Вы писали:
ARK>С чего вы взяли? Это особенность реализации. ARK>В Java так и пишут — "implements", а наследование делается через другой синтаксис.
И что? Причем тут наследование? Я про сабтайпинг говорю.
K>>Между тайпклассом и типом, для которого определен его инстанс такого отношения нет. Т.е. скастить к тип к тайпклассу нельзя.
ARK>Ну так есть экзистенциальные типы, которые по сути есть то же самое, вид сбоку.
Нет, не то же самое. По области применения некоторое пересечение есть.
'You may call it "nonsense" if you like, but I'VE heard nonsense, compared with which that would be as sensible as a dictionary!' (c) Lewis Carroll
Здравствуйте, Klapaucius, Вы писали:
K>На этом этапе думаю уже понятно, что рассуждающие про сиплюсплюсные темплейты и концепты по соседству не понимают о чем говорят, ничего общего все это с параметрическим полиморфизмом и классами типов не имеет.
В том виде, в котором ты все сформулировал, это просто древняя сиплюсовая техника traits: K>
K>class Add a where
K> plus :: a -> a -> a
K> zero :: a
K>instance Add Int where
K> plus = (+#)
K> zero = 0#
K>instance Add Double where
K> plus = (+##)
K> zero = 0.0#
K>
template<class a> struct Add; // без реализации, чтоб была ошибка компиляции для неизвестного типаtemplate<> struct Add<int> {
static int plus(int x, int y) { return x+y; }
static int zero() { return 0; }
};
template<> struct Add<double> {
static double plus(double x, double y) { return x+y; }
static double zero() { return 0.0; }
};
K>Рассахаривается именно в описанные выше значения-словари. K>Сигнатура функции sum меняется таким образом: K>
K>sum :: Add a => [a] -> a
K>
template<class a>
int sum(vector<a> v) {
return std::accumulate( std::begin(v), std::end(v), Add<a>::plus, Add<a>::zero() );
}
причем std::begin и std::end — это тоже они (синтаксис несущественно упрощен):
Здравствуйте, jazzer, Вы писали:
J>В том виде, в котором ты все сформулировал, это просто древняя сиплюсовая техника traits:
Проблема в том, что если написать так:
Здравствуйте, WolfHound, Вы писали:
WH>Здравствуйте, jazzer, Вы писали:
J>>В том виде, в котором ты все сформулировал, это просто древняя сиплюсовая техника traits: WH>Проблема в том, что если написать так: WH>
WH>Ошибка будет не при компиляции Add<float>, а при компиляции sum.
Естественно, потому что компиляция Add<float> происходит при компиляции sum.
Но можно это тривиально запихнуть в сигнатуру sum и получать ошибку в месте вызова, до компиляции самой функции
Просто не хотел перегружать пост деталями.
В любом случае будет ошибка вида "Add<float>::plus не определен".
Или ты имел в виду, что не будет отловлена ошибка определения самого Add<float> (отсутствие plus), до всяких sum и прочих пользователей?
Если да, то тут тоже можно придумать как извернуться (static_assert), но в любом случае это второстепенная проблема.
Потому что главное — что будет сгенерирована ошибка во время компиляции. В какой конкретно момент компиляции — это уже детали.
Здравствуйте, jazzer, Вы писали:
J>Потому что главное — что будет сгенерирована ошибка во время компиляции. В какой конкретно момент компиляции — это уже детали.
Из-за этой детали мы получаем:
1)Отсутствие модульности. Мы не можем отдельно скомпилировать Add<float> и sum. После чего подключить их к третьему модулю и использовать совместно.
2)Кошмарные сообщения об ошибках. Причем как правило далеко от того места где была реальная ошибка.
... << RSDN@Home 1.2.0 alpha 5 rev. 62>>
Пусть это будет просто:
просто, как только можно,
но не проще.
(C) А. Эйнштейн
Здравствуйте, WolfHound, Вы писали:
WH>Здравствуйте, jazzer, Вы писали:
J>>Потому что главное — что будет сгенерирована ошибка во время компиляции. В какой конкретно момент компиляции — это уже детали. WH>Из-за этой детали мы получаем: WH>1)Отсутствие модульности. Мы не можем отдельно скомпилировать Add<float> и sum. После чего подключить их к третьему модулю и использовать совместно.
Во-первых, можем, если все написано правильно. А если неправильно — то всяко использовать не сможем — компиляция сломается, хоть в С++, хоть в Хаскелле, просто в разных местах.
Во-вторых, это мелочи. Достаточно static_assert вставить после объявления, чтоб все валилось сразу, раз уж это так для тебя принципиально. (А концепты, вроде как, сразу все проверяют автоматом)
WH>2)Кошмарные сообщения об ошибках. Причем как правило далеко от того места где была реальная ошибка.
Концепты именно это и исправляют (причем эмуляции концептов есть и сейчас, как и (очень простые) техники по уменьшению количества печатаемых ошибок, просто их писать неудобно).
Так что ты говоришь о какимх-то мелочах, которых разруливаются так или иначе, в то время как Klapaucius говорил о том, что плюсовые шаблоны/концепты — это в принципе не тайпклассы.
Я очень сомневаюсь, что раннее сообщение об ошибке — это принципиальная вещь для него. По крайней мере, он говорил совершенно о других вещах.
Здравствуйте, jazzer, Вы писали:
J>Так что ты говоришь о какимх-то мелочах, которых разруливаются так или иначе, в то время как Klapaucius говорил о том, что плюсовые шаблоны/концепты — это в принципе не тайпклассы. J>Я очень сомневаюсь, что раннее сообщение об ошибке — это принципиальная вещь для него. По крайней мере, он говорил совершенно о других вещах.
Здравствуйте, samius, Вы писали:
S>Здравствуйте, jazzer, Вы писали:
J>>Так что ты говоришь о какимх-то мелочах, которых разруливаются так или иначе, в то время как Klapaucius говорил о том, что плюсовые шаблоны/концепты — это в принципе не тайпклассы. J>>Я очень сомневаюсь, что раннее сообщение об ошибке — это принципиальная вещь для него. По крайней мере, он говорил совершенно о других вещах.
S>Если что, есть работы на эту тему. S>C++ Templates/Traits versus Haskell Type Classes (2005)
А сама статья есть где-нибудь? А то по ссылке только то, что она цитирует...
Здравствуйте, jazzer, Вы писали:
J>Здравствуйте, samius, Вы писали:
J>А сама статья есть где-нибудь? А то по ссылке только то, что она цитирует...
у меня там же ссылка на PDF видна