Что такого плохого в шаблоне Haskell?
Кажется, что шаблон Haskell часто рассматривается сообществом Haskell как досадное удобство. Трудно передать словами то, что я наблюдал в этом отношении, но рассмотрим эти несколько примеров.
- Шаблон Haskell указан в разделе "Гадкий (но необходимый)" в ответ на вопрос: Какие расширения Haskell (GHC) следует использовать / избегать пользователям?
- Шаблон Haskell считается временным / неполноценным решением в Unboxed Векторы потока значений newtype'd (список рассылки библиотек)
- Yesod часто критикуют за то, что он слишком полагается на Template Haskell (см. Сообщение в блоге в ответ на это мнение)
Я видел различные посты в блоге, где люди делают довольно аккуратные вещи с Template Haskell, позволяя использовать более симпатичный синтаксис, который просто невозможен в обычном Haskell, а также огромное сокращение шаблонов. Так почему же на шаблон Хаскелла смотрят свысока? Что делает это нежелательным? При каких обстоятельствах следует избегать Template Haskell и почему?
6 ответов
Одна из причин, по которой следует избегать Template Haskell, заключается в том, что он в целом не является типобезопасным, что противоречит большей части "духа Haskell". Вот несколько примеров этого:
- Вы не можете контролировать, какой тип Haskell AST будет генерировать часть кода TH, за пределами того, где он появится; Вы можете иметь значение типа
Exp
, но вы не знаете, если это выражение, которое представляет собой[Char]
или(a -> (forall b . b -> c))
или что угодно. TH был бы более надежным, если бы можно было выразить, что функция может генерировать только выражения определенного типа, или только объявления функций, или только шаблоны соответствия конструкторам данных и т. Д. - Вы можете генерировать выражения, которые не компилируются. Вы сгенерировали выражение, которое ссылается на свободную переменную
foo
что не существует? Не повезло, вы увидите это только при использовании генератора кода и только в тех обстоятельствах, которые инициируют генерацию этого конкретного кода. Модульное тестирование тоже очень сложно.
ТХ тоже откровенно опасен
- Код, который выполняется во время компиляции, может делать произвольно
IO
включая запуск ракет или кражу вашей кредитной карты. Вам не нужно просматривать все загруженные пакеты, чтобы найти эксплойты TH. - TH может получить доступ к частным функциям и определениям модуля, в некоторых случаях полностью нарушая инкапсуляцию.
Тогда есть некоторые проблемы, которые делают функции TH менее увлекательными для использования в качестве разработчика библиотеки:
- Код TH не всегда может быть компонован. Допустим, кто-то делает генератор для объективов, и чаще всего этот генератор будет структурирован таким образом, что его может вызывать только "конечный пользователь", а не другой код TH, например, принимая список конструкторов типов для генерации линз в качестве параметра. Сложно генерировать этот список в коде, в то время как пользователю остается только написать
generateLenses [''Foo, ''Bar]
, - Разработчики даже не знают, что код TH может быть составлен. Знаете ли вы, что вы можете написать
forM_ [''Foo, ''Bar] generateLens
?Q
это просто монада, так что вы можете использовать все обычные функции на нем. Некоторые люди этого не знают, и из-за этого они создают несколько перегруженных версий по существу одних и тех же функций с одинаковыми функциями, и эти функции приводят к определенному эффекту раздувания. Кроме того, большинство людей пишут свои генераторы вQ
монада, даже если они не должны, что походит на написаниеbla :: IO Int; bla = return 3
; вы предоставляете функции больше "среды", чем нужно, и клиенты этой функции обязаны предоставлять эту среду как результат этого.
Наконец, есть некоторые вещи, которые делают функции TH менее увлекательными для использования в качестве конечного пользователя:
- Непрозрачность. Когда функция TH имеет тип
Q Dec
, он может генерировать абсолютно все на верхнем уровне модуля, и у вас нет абсолютно никакого контроля над тем, что будет сгенерировано. - Монолитность. Вы не можете контролировать, сколько генерирует функция TH, если разработчик не позволяет это; если вы найдете функцию, которая генерирует интерфейс базы данных и интерфейс сериализации JSON, вы не сможете сказать: "Нет, мне нужен только интерфейс базы данных, спасибо; я буду катать свой собственный интерфейс JSON"
- Время выполнения. Код TH занимает относительно много времени для запуска. Код интерпретируется заново каждый раз, когда файл компилируется, и часто для выполнения загружаемого кода TH требуется тонна пакетов, которые необходимо загрузить. Это значительно замедляет время компиляции.
Это исключительно мое собственное мнение.
Это некрасиво использовать.
$(fooBar ''Asdf)
просто не выглядит красиво. Поверхностно, конечно, но это способствует.Писать еще страшнее. Цитирование иногда срабатывает, но большую часть времени вам приходится делать прививку AST вручную и слесарное дело. API является большим и громоздким, всегда есть много случаев, о которых вы не заботитесь, но все еще нуждаетесь в отправке, и случаи, о которых вы заботитесь, обычно присутствуют в нескольких похожих, но не идентичных формах (данные против нового типа, запись стиль против нормальных конструкторов и т. д.). Писать скучно и многократно, достаточно сложно, чтобы не быть механическим. Предложение по реформе решает некоторые из этих вопросов (делая цитаты более применимыми).
Сценическое ограничение - ад. Неспособность объединить функции, определенные в одном и том же модуле, является меньшей его частью: другое следствие - если у вас есть объединение верхнего уровня, все, что находится после него в модуле, будет вне области действия чего-либо до него. Другие языки с этим свойством (C, C++) делают его работоспособным, позволяя вам перенаправлять объявления, но Haskell этого не делает. Если вам нужны циклические ссылки между объединенными объявлениями или их зависимостями и зависимостями, вы обычно просто облажались.
Это недисциплинировано. Под этим я подразумеваю, что в большинстве случаев, когда вы выражаете абстракцию, за этой абстракцией стоит какой-то принцип или концепция. Для многих абстракций принцип, лежащий в их основе, может быть выражен в их типах. Для классов типов вы можете часто формулировать законы, которым должны подчиняться экземпляры и которые могут принимать клиенты. Если вы используете новую универсальную функцию GHC для абстрагирования формы объявления экземпляра над любым типом данных (в пределах границ), вы скажете: "Для типов суммы это работает так, для типов продукта - так". Шаблон Haskell, с другой стороны, является просто макросом. Это не абстракция на уровне идей, а абстракция на уровне AST, что лучше, но скромно, чем абстракция на уровне простого текста.*
Это связывает вас с GHC. Теоретически другой компилятор мог бы реализовать это, но на практике я сомневаюсь, что это когда-нибудь случится. (Это в отличие от различных расширений системы типов, которые, хотя они могут быть реализованы только GHC на данный момент, я легко мог себе представить, что они будут приняты другими компиляторами в будущем и в конечном итоге стандартизированы.)
API не стабилен. Когда в GHC добавляются новые языковые функции, а пакет template-haskell обновляется для их поддержки, это часто связано с несовместимыми с обратным характером изменениями типов данных TH. Если вы хотите, чтобы ваш код TH был совместим с более чем одной версией GHC, вам нужно быть очень осторожным и, возможно, использовать
CPP
,Есть общий принцип, что вы должны использовать правильный инструмент для работы и самый маленький, который будет достаточным, и в этой аналогии шаблон Haskell выглядит примерно так. Если есть способ сделать это, но это не Template Haskell, это, как правило, предпочтительнее.
Преимущество Template Haskell состоит в том, что вы можете делать с ним вещи, которые вы не могли бы сделать другим способом, и это большое. В большинстве случаев то, для чего используется TH, могло бы быть сделано иначе, только если они были реализованы непосредственно как функции компилятора. У TH чрезвычайно выгодно иметь и то и другое, потому что он позволяет вам делать эти вещи, и потому что он позволяет вам создавать прототипы потенциальных расширений компилятора гораздо более легким и многократно используемым способом (см., Например, различные пакеты объективов).
Подводя итог, почему я думаю, что существуют негативные чувства по отношению к шаблону Haskell: он решает множество проблем, но для любой конкретной проблемы, которую он решает, создается впечатление, что должно быть лучшее, более элегантное, дисциплинированное решение, лучше подходящее для решения этой проблемы, тот, который не решает проблему, автоматически генерируя шаблон, но устраняя необходимость иметь шаблон.
* Хотя я часто это чувствую CPP
имеет лучшее отношение мощности к весу для тех проблем, которые он может решить.
РЕДАКТИРОВАТЬ 23-04-14: То, что я часто пытался получить в вышеизложенном, и только недавно понял точно, что есть важное различие между абстракцией и дедупликацией. Правильная абстракция часто приводит к дедупликации как побочному эффекту, а дублирование часто является явным признаком неадекватной абстракции, но это не то, почему это ценно. Правильная абстракция - это то, что делает код правильным, понятным и обслуживаемым. Дедупликация только делает ее короче. Шаблон Haskell, как и макросы в целом, является инструментом для дедупликации.
Я хотел бы остановиться на нескольких моментах, которые поднимает dflemstr.
Я не нахожу тот факт, что вы не можете перепроверять TH, чтобы это беспокоило. Зачем? Потому что даже если будет ошибка, все равно будет время компиляции. Я не уверен, усиливает ли это мои аргументы, но по духу это похоже на ошибки, которые вы получаете при использовании шаблонов в C++. Я думаю, что эти ошибки более понятны, чем ошибки C++, поскольку вы получите довольно напечатанную версию сгенерированного кода.
Если выражение / квазиквотер TH делает что-то настолько продвинутое, что хитрые углы могут скрыться, то, возможно, это опрометчиво?
Я немного нарушаю это правило с помощью квазиквотеров, над которыми я работал в последнее время (используя haskell-src-exts / meta) - https://github.com/mgsloan/quasi-extras/tree/master/examples. Я знаю, что это приводит к некоторым ошибкам, таким как неспособность объединить обобщенные списки. Тем не менее, я думаю, что есть хороший шанс, что некоторые идеи из http://hackage.haskell.org/trac/ghc/blog/Template%20Haskell%20Proposal окажутся в компиляторе. До тех пор библиотеки для разбора Haskell на TH-деревья являются почти идеальным приближением.
Что касается скорости / зависимостей компиляции, мы можем использовать "нулевой" пакет для вставки сгенерированного кода. Это, по крайней мере, хорошо для пользователей данной библиотеки, но мы не можем добиться большего успеха в случае редактирования библиотеки. Могут ли зависимости TH раздувать генерируемые двоичные файлы? Я думал, что это исключило все, на что не ссылается скомпилированный код.
Ограничение этапов / разбиение этапов компиляции модуля Haskell отстой.
RE Непрозрачность: то же самое для любой библиотечной функции, которую вы вызываете. Вы не можете контролировать, что будет делать Data.List.groupBy. У вас просто есть разумная "гарантия" / соглашение о том, что номера версий говорят вам о совместимости. Это несколько другой вопрос изменений, когда.
Именно здесь использование нуля окупается - вы уже создаете версии сгенерированных файлов - так что вы всегда будете знать, когда изменилась форма сгенерированного кода. Тем не менее, просмотр больших различий может показаться немного сложным для большого количества сгенерированного кода, так что это единственное место, где полезен лучший интерфейс разработчика.
RE Монолитизм: Вы можете, конечно, постобработать результаты выражения TH, используя свой собственный код времени компиляции. Было бы не так уж много кода для фильтрации по типу / имени объявления верхнего уровня. Черт возьми, вы можете себе представить написание функции, которая делает это в общем. Для модификации / демонолитизации квазиквотеров вы можете сопоставить шаблон с "QuasiQuoter" и извлечь использованные преобразования или сделать новые с точки зрения старых.
Этот ответ является ответом на вопросы, поднятые Иллисусом, пункт за пунктом:
- Это некрасиво использовать. $(fooBar ''Asdf) просто не выглядит красиво. Поверхностно, конечно, но это способствует.
Согласен. Я чувствую, что $() был выбран, чтобы выглядеть так, как будто он был частью языка - используя знакомую символьную палитру Хаскелла. Тем не менее, это именно то, что вы / не хотите / хотите в символах, используемых для сплайсинга макросов. Они определенно смешиваются слишком много, и этот косметический аспект очень важен. Мне нравится внешний вид {{ }} для сростков, потому что они довольно визуально различимы.
- Писать еще страшнее. Цитирование иногда срабатывает, но большую часть времени вам приходится делать прививку AST вручную и слесарное дело. [API][1] является большим и громоздким, всегда есть много случаев, о которых вы не заботитесь, но все еще нуждаетесь в отправке, и случаи, о которых вы заботитесь, обычно присутствуют в нескольких похожих, но не идентичных формах (данные против нового типа, стиля записи против обычных конструкторов и т. д.). Писать скучно и многократно, достаточно сложно, чтобы не быть механическим. [Предложение о реформе][2] решает некоторые из этих вопросов (делая цитаты более применимыми).
Я также согласен с этим, однако, как отмечают некоторые из комментариев в "Новых направлениях для TH", отсутствие хороших готовых цитат AST не является критическим недостатком. В этом пакете WIP я пытаюсь решить эти проблемы в виде библиотеки: https://github.com/mgsloan/quasi-extras. До сих пор я позволяю соединять в нескольких местах больше, чем обычно, и могу сопоставлять шаблоны на AST.
- Сценическое ограничение - ад. Неспособность объединить функции, определенные в одном и том же модуле, является меньшей его частью: другое следствие - если у вас есть объединение верхнего уровня, все, что находится после него в модуле, будет вне области действия чего-либо до него. Другие языки с этим свойством (C, C++) делают его работоспособным, позволяя вам перенаправлять объявления, но Haskell этого не делает. Если вам нужны циклические ссылки между объединенными объявлениями или их зависимостями и зависимостями, вы обычно просто облажались.
Я столкнулся с проблемой невозможности определения циклического TH раньше... Это довольно раздражает. Есть решение, но оно уродливо - оберните вещи, связанные с циклической зависимостью, в выражение TH, которое объединяет все сгенерированные объявления. Одним из таких генераторов объявлений может быть просто квазиквотер, который принимает код на Haskell.
- Это беспринципно. Под этим я подразумеваю, что в большинстве случаев, когда вы выражаете абстракцию, за этой абстракцией стоит какой-то принцип или концепция. Для многих абстракций принцип, лежащий в их основе, может быть выражен в их типах. Когда вы определяете класс типов, вы часто можете сформулировать законы, которым должны подчиняться экземпляры и которые могут принимать клиенты. Если вы используете [новую универсальную функцию] GHC [3], чтобы абстрагировать форму объявления экземпляра для любого типа данных (в пределах границ), вы можете сказать: "для типов суммы это работает так, для типов продукта - это работает так ". Но Template Haskell - это просто тупые макросы. Это не абстракция на уровне идей, а абстракция на уровне AST, которая лучше, но скромно, чем абстракция на уровне простого текста.
Это только беспринципно, если вы делаете с ним беспринципные вещи. Единственное отличие состоит в том, что благодаря механизмам абстракции, реализованным в компиляторе, у вас больше уверенности в том, что абстракция не протекает. Возможно, демократизация языкового дизайна звучит немного страшно! Создатели библиотек TH должны хорошо документировать и четко определять значение и результаты инструментов, которые они предоставляют. Хорошим примером принципиального TH является производный пакет: http://hackage.haskell.org/package/derive - он использует DSL, так что пример многих дериваций / задает / фактическое деривация.
- Это связывает вас с GHC. Теоретически другой компилятор мог бы реализовать это, но на практике я сомневаюсь, что это когда-нибудь случится. (Это в отличие от различных расширений системы типов, которые, хотя они могут быть реализованы только GHC на данный момент, я легко мог себе представить, что они будут приняты другими компиляторами в будущем и в конечном итоге стандартизированы.)
Это очень хороший момент - TH API довольно большой и неуклюжий. Реализация кажется, что это может быть сложно. Тем не менее, есть только несколько способов решить проблему представления AST на Haskell. Я полагаю, что копирование TH ADTs и написание конвертера во внутреннее представление AST поможет вам в этом. Это было бы эквивалентно (что немаловажно) усилиям по созданию haskell-src-meta. Это также можно было бы просто повторно реализовать, просто распечатав TH AST и используя внутренний анализатор компилятора.
Хотя я могу ошибаться, я не вижу в TH сложность расширения компилятора с точки зрения реализации. Это на самом деле одно из преимуществ "сохранения простоты" и отсутствия фундаментального уровня в некоторой теоретически привлекательной, статически проверяемой шаблонной системе.
- API не стабилен. Когда в GHC добавляются новые языковые функции, а пакет template-haskell обновляется для их поддержки, это часто связано с несовместимыми с обратным характером изменениями типов данных TH. Если вы хотите, чтобы ваш код TH был совместим с более чем одной версией GHC, вам нужно быть очень осторожным и, возможно, использовать
CPP
,
Это тоже хороший момент, но несколько драматичный. Хотя в последнее время были добавлены API, они не вызывали частых поломок. Кроме того, я думаю, что с превосходным цитированием AST, которое я упоминал ранее, API, который фактически должен использоваться, может быть значительно уменьшен. Если никакое конструирование / сопоставление не требует отдельных функций и вместо этого выражается в виде литералов, то большая часть API исчезает. Более того, код, который вы пишете, будет легче переносить в представления AST для языков, подобных Haskell.
Подводя итог, я думаю, что TH является мощным, полу забытым инструментом. Меньшее количество ненависти может привести к созданию более оживленной экосистемы библиотек, способствующей внедрению большего количества прототипов языковых функций. Было замечено, что TH- это мощный инструмент, который может позволить вам / делать / почти все. Анархия! Что ж, по моему мнению, эта мощь может позволить вам преодолеть большинство его ограничений и создавать системы, способные к совершенно принципиальным подходам метапрограммирования. Стоит использовать уродливые хаки для имитации "правильной" реализации, поскольку таким образом дизайн "правильной" реализации будет постепенно становиться понятным.
В моей личной идеальной версии нирваны, большая часть языка фактически переместилась бы из компилятора в библиотеки этого разнообразия. Тот факт, что функции реализованы в виде библиотек, не сильно влияет на их способность точно абстрагироваться.
Какой типичный ответ Хаскелла на стандартный код? Абстракция. Какие наши любимые абстракции? Функции и классы типов!
Классы типов позволяют нам определить набор методов, которые затем могут использоваться во всех видах функций, общих для этого класса. Однако, кроме этого, единственный способ, которым классы помогают избежать шаблонов, это предлагая "определения по умолчанию". Теперь вот пример беспринципной возможности!
Наборы минимальных привязок не объявляются / проверяется компилятором. Это может привести к непреднамеренным определениям, дающим основание из-за взаимной рекурсии.
Несмотря на большое удобство и мощь, которые это даст, вы не можете указать значения по умолчанию для суперкласса, поскольку экземпляры-сироты http://lukepalmer.wordpress.com/2009/01/25/a-world-without-orphans/ Это позволит нам исправить числовая иерархия изящно!
Переход к TH-подобным возможностям по умолчанию для методов привел к http://www.haskell.org/haskellwiki/GHC.Generics. Хотя это классная штука, мой единственный опыт отладки кода с использованием этих обобщений был почти невозможен из-за размера типа индуцированного для и ADT, такого сложного, как AST. https://github.com/mgsloan/th-extra/commit/d7784d95d396eb3abdb409a24360beb03731c88c
Другими словами, это пошло после функций, предоставляемых TH, но ему пришлось поднять всю область языка, язык конструирования, в представление системы типов. Хотя я вижу, что это хорошо работает для вашей общей проблемы, для сложных, кажется, она склонна приносить кучу символов, гораздо более ужасающих, чем хакерские игры TH.
TH дает вам вычисление выходного кода во время компиляции на уровне значений, в то время как generics заставляет вас поднять часть кода для сопоставления с образцом / рекурсии в систему типов. Хотя это ограничивает пользователя несколькими довольно полезными способами, я не думаю, что сложность того стоит.
Я думаю, что отказ от TH и метапрограммирование, подобное lisp, привело к предпочтению таких вещей, как метод-значения по умолчанию, а не к более гибкому, макро-расширению, как объявления экземпляров. Дисциплина избегания вещей, которые могут привести к непредвиденным результатам, является разумной, однако мы не должны игнорировать то, что способная система типов Хаскелла допускает более надежное метапрограммирование, чем во многих других средах (путем проверки сгенерированного кода).
Одна довольно прагматичная проблема с шаблоном Haskell состоит в том, что он работает только тогда, когда доступен интерпретатор байт-кода GHC, что не имеет место на всех архитектурах. Поэтому, если ваша программа использует Template Haskell или использует библиотеки, которые ее используют, она не будет работать на компьютерах с процессором ARM, MIPS, S390 или PowerPC.
На практике это актуально: git-annex - это инструмент, написанный на Haskell, который имеет смысл запускать на машинах, которые беспокоятся о хранилище, такие машины часто имеют не i386-CPU. Лично я запускаю git-annex на NSLU 2 (32 МБ ОЗУ, 266 МГц ЦП; знаете ли вы, что Haskell отлично работает на таком оборудовании?) Если он будет использовать Template Haskell, это невозможно.
(Ситуация с GHC на ARM в наши дни значительно улучшается, и я думаю, что 7.4.2 даже работает, но точка зрения остается неизменной).
Почему TH плох? Для меня все сводится к следующему:
Если вам нужно создать столько повторяющегося кода, что вы пытаетесь использовать TH для его автоматической генерации, вы делаете это неправильно!
Думаю об этом. Половина привлекательности Haskell заключается в том, что его высокоуровневый дизайн позволяет вам избежать огромного количества бесполезного шаблонного кода, который вы должны писать на других языках. Если вам нужна генерация кода во время компиляции, вы в основном говорите, что ваш язык или дизайн вашего приложения не помогли вам. И мы, программисты, не любим проваливаться.
Иногда, конечно, это необходимо. Но иногда вы можете избежать необходимости TH, просто будучи немного умнее со своими проектами.
(Другое дело, что TH довольно низкоуровневый. Здесь нет грандиозного высокоуровневого дизайна; раскрыты многие детали внутренней реализации GHC. И это делает API склонным к изменениям...)