Что плохого в шаблоне Haskell?

Кажется, что Template Haskell часто рассматривается сообществом Haskell как неудачное удобство. Трудно выразить словами то, что я наблюдал в этом отношении, но рассмотрим эти несколько примеров

Я видел различные записи в блогах, где люди делают довольно аккуратные вещи с Template Haskell, позволяя более красивый синтаксис, который просто невозможен в регулярном Haskell, а также потрясающее сокращение шаблонов. Так почему же Template Haskell выглядит таким образом? Что делает его нежелательным? При каких обстоятельствах следует избегать шаблона Haskell и почему?

Ответ 1

Одна из причин отказа от шаблона Haskell заключается в том, что он в целом не является безопасным для типа, тем самым, противоречит большей части "духа Haskell". Вот несколько примеров этого:

  • Вы не можете контролировать, какой Haskell AST будет генерировать часть кода TH, за которой он появится; вы можете иметь значение типа Exp, но вы не знаете, является ли это выражением, которое представляет [Char] или (a -> (forall b . b -> c)) или что-то еще. TH был бы более надежным, если бы можно было бы сказать, что функция может генерировать выражения только определенного типа, или только объявления функций, или только шаблоны сопоставления данных и т.д.
  • Вы можете создавать выражения, которые не компилируются. Вы создали выражение, которое ссылается на свободную переменную foo, которая не существует? Жесткая удача, вы увидите, что при использовании своего генератора кода и только при обстоятельствах, которые вызывают генерацию этого конкретного кода. Это очень сложно для unit test.

TH также абсолютно опасен:

  • Код, который выполняется во время компиляции, может выполнять произвольные IO, включая запуск ракеты или кражу вашей кредитной карты. Вы не хотите просматривать каждый пакет cabal, который вы когда-либо скачивали, в поисках эксплойтов TH.
  • TH может получить доступ к функциям и определениям "модуль-частный", полностью блокируя инкапсуляцию в некоторых случаях.

Тогда есть некоторые проблемы, которые делают функции TH менее забавными для использования в качестве разработчика библиотеки:

  • Код TH не всегда сложен. Скажем, кто-то делает генератор для объективов, и чаще всего этот генератор будет структурирован таким образом, что он может быть вызван непосредственно "конечным пользователем", а не другим кодом TH, например, принимая список конструкторов типов для генерации объективов в качестве параметра. Трудно сгенерировать этот список в коде, тогда как пользователю нужно написать generateLenses [''Foo, ''Bar].
  • Разработчики даже не знают , что код TH может быть составлен. Знаете ли вы, что можете написать forM_ [''Foo, ''Bar] generateLens? Q - это просто монада, поэтому вы можете использовать на ней все обычные функции. Некоторые люди этого не знают, и из-за этого они создают несколько перегруженных версий, по существу, с теми же функциями с одинаковой функциональностью, и эти функции приводят к определенному эффекту вздутия. Кроме того, большинство людей пишут свои генераторы в монаде Q, даже если это не так, как написано bla :: IO Int; bla = return 3; вы предоставляете функции больше "среды", чем нужно, и клиенты этой функции должны обеспечивать эту среду как результат этого.

Наконец, есть некоторые вещи, которые делают функции TH менее интересными для использования в качестве конечного пользователя:

  • Opacity. Когда функция TH имеет тип Q Dec, она может генерировать абсолютно что-либо на верхнем уровне модуля, и у вас нет абсолютно никакого контроля над тем, что будет сгенерировано.
  • монолитность. Вы не можете контролировать, какая функция TH генерируется, если разработчик не позволяет это; если вы найдете функцию, которая генерирует интерфейс базы данных и, интерфейс сериализации JSON, вы не можете сказать "Нет, мне нужен интерфейс базы данных, спасибо, я откажу свой собственный интерфейс JSON"
  • Время выполнения. Код TH занимает относительно много времени для запуска. Код интерпретируется заново каждый раз, когда файл скомпилирован, и часто для исполняемого кода TH требуется тонна пакетов, которые необходимо загрузить. Это значительно замедляет время компиляции.

Ответ 2

Это единственное мое мнение.

  • Это уродливо использовать. $(fooBar ''Asdf) просто не выглядит красиво. Поверхностно, конечно, но это способствует.

  • Это даже уродливее писать. Иногда цитаты работают, но в то же время вы должны выполнять ручную трансплантацию АСТ и сантехнику. API большой и громоздкий, всегда много дел, которые вам не нужны, но все равно нужно отправлять, а случаи, о которых вы заботитесь, обычно присутствуют в нескольких похожих, но не идентичных формах (данные против newtype, record - стиль против обычных конструкторов и т.д.). Он скучный и повторяющийся, чтобы писать и достаточно сложно, чтобы не быть механическим. Предложение по реформе касается некоторых из них (более широко применимые котировки).

  • Сценическое ограничение - ад. Невозможность сплайсинга функций, определенных в том же модуле, - это меньшая его часть: другим следствием является то, что если у вас есть сращивание на верхнем уровне, все после него в модуле будет вне сферы видимости перед чем-либо. Другие языки с этим свойством (C, C++) делают его работоспособным, позволяя вам пересылать объявления, но Haskell этого не делает. Если вам нужны циклические ссылки между объявлениями сплайсинга или их зависимостями и иждивенцами, вы обычно просто ввернуты.

  • Он недисциплинирован. Я имею в виду, что большую часть времени, когда вы выражаете абстракцию, для этой абстракции есть какой-то принцип или понятие. Для многих абстракций принцип, лежащий в их основе, может быть выражен в их типах. Для классов типов вы часто можете формулировать законы, которые должны соблюдаться, и клиенты могут предполагать. Если вы используете функцию новых дженериков GHC, чтобы абстрагировать форму объявления экземпляра по любому типу данных (в пределах границ), вы можете сказать "для типов сумм, он работает так, для типов продуктов, он работает так". Шаблон Haskell, с другой стороны, это просто макросы. Это не абстракция на уровне идей, а абстракция на уровне АСТ, что лучше, но лишь скромно, чем абстракция на уровне обычного текста. *

  • Это связывает вас с GHC. Теоретически другой компилятор мог бы его реализовать, но на практике я сомневаюсь, что это когда-нибудь произойдет. (Это противоречит разным типам системных расширений, которые, хотя они могут быть реализованы только GHC в настоящий момент, я легко мог представить, что их усыновили другие компиляторы по дороге и в конечном итоге стандартизированы).

  • API нестабилен. Когда новые функции языка добавляются в GHC, а пакет template-haskell обновляется для их поддержки, это часто включает в себя обратные несовместимые изменения типов данных TH. Если вы хотите, чтобы ваш код TH был совместим с более чем одной версией GHC, вам нужно быть очень осторожным и, возможно, использовать CPP.

  • Существует общий принцип, что вы должны использовать правильный инструмент для работы и наименьший, который будет достаточным, и в этой аналогии Template Haskell- то вроде этого. Если есть способ сделать это, а не Template Haskell, это вообще предпочтительнее.

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

Подводя итог, почему я думаю, что есть отрицательные чувства к Template Haskell: он решает много проблем, но для любой заданной проблемы, которую он решает, кажется, что должно быть лучшее, более элегантное, дисциплинированное решение, более подходящее для решения этой проблемы, тот, который не решает проблему, автоматически генерируя шаблон, но устраняя необходимость наличия шаблона.

* Хотя я часто чувствую, что CPP имеет лучшее соотношение мощности к весу для тех проблем, которые он может решить.

EDIT 23-04-14: То, что я часто пытаюсь понять в приведенном выше, и только недавно получил точно, заключается в том, что существует важное различие между абстракцией и дедупликацией. Надлежащая абстракция часто приводит к дедупликации как побочному эффекту, а дублирование часто является признаком неадекватной абстракции, но это не то, почему оно ценно. Надлежащая абстракция - это то, что делает код правильным, понятным и поддерживаемым. Дедупликация только делает ее короче. Шаблон Haskell, как и макросы в целом, является инструментом для дедупликации.

Ответ 3

Я хотел бы затронуть несколько точек, которые вызывает dflemstr.

Я не считаю, что вы не можете сказать, что TH это беспокоит. Зачем? Потому что, даже если есть ошибка, все равно будет время компиляции. Я не уверен, что это усиливает мои аргументы, но это по духу аналогично ошибкам, которые вы получаете при использовании шаблонов на С++. Я думаю, что эти ошибки более понятны, чем ошибки С++, хотя вы получите довольно печатную версию сгенерированного кода.

Если выражение TH/quasi-quoter делает что-то настолько продвинутое, что хитрые углы могут скрыть, то, возможно, это не рекомендуется?

Я довольно сильно нарушаю это правило с квазициклами, с которыми я работал в последнее время (используя 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 Opacity: это то же самое для любой функции библиотеки, которую вы вызываете. У вас нет контроля над тем, что сделает Data.List.groupBy. У вас просто есть разумная "гарантия" /соглашение о том, что номера версий сообщают вам что-то о совместимости. Это несколько другое изменение, когда.

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

RE Монолитизм: вы можете, конечно, выполнить пост-обработку результатов выражения TH, используя свой собственный код времени компиляции. Кодирование кода не должно быть очень сильным для типа/имени декларации верхнего уровня. Черт, вы могли бы представить себе функцию, которая делает это в целом. Для модификации/де-монолитизации квазиквадраторов вы можете сопоставлять совпадение на "QuasiQuoter" и извлекать использованные преобразования или создавать новый в терминах старого.

Ответ 4

Этот ответ в ответ на вопросы, поднятые illissius, по пунктам:

  • Это уродливо использовать. $(fooBar '' Asdf) просто не выглядит красиво. Поверхностно, конечно, но это способствует.

Я согласен. Мне кажется, что $() выбрано так, чтобы выглядеть как часть языка - используя знакомый поддон Haskell. Тем не менее, именно то, что вы/не хотите/не хотите в символах, используемых для вашего сращивания макросов. Они определенно сливаются в слишком много, и этот косметический аспект очень важен. Мне нравится внешний вид {{}} для сращиваний, потому что они довольно визуально различны.

  • Это даже уродливее писать. Иногда цитаты работают, но в то же время вы должны выполнять ручную трансплантацию АСТ и сантехнику. [API] [1] большой и громоздкий, всегда есть много случаев, о которых вам не безразлично, но все равно нужно отправлять, а случаи, о которых вы заботитесь, обычно присутствуют в нескольких похожих, но не идентичных формах (данные vs newtype, стиль записи и обычные конструкторы и т.д.). Он скучный и повторяющийся, чтобы писать и достаточно сложно, чтобы не быть механическим. [Предложение по реформе] [2] касается некоторых из них (использование более широко применимых котировок).

Я также согласен с этим, однако, как замечают некоторые из комментариев в "New Directions for TH", отсутствие хорошего внеконтактного цитирования АСТ не является критическим недостатком. В этом пакете WIP я пытаюсь решить эти проблемы в библиотечной форме: https://github.com/mgsloan/quasi-extras. До сих пор я разрешаю сращивание в нескольких местах, чем обычно, и может сопоставлять шаблон по AST.

  • Сценическое ограничение - ад. Невозможность сплайсинга функций, определенных в том же модуле, - это меньшая его часть: другим следствием является то, что если у вас есть сращивание на верхнем уровне, все после него в модуле будет вне видимости чего-либо перед ним. Другие языки с этим свойством (C, С++) делают его работоспособным, позволяя вам пересылать объявления, но Haskell этого не делает. Если вам нужны циклические ссылки между объявлениями сплайсинга или их зависимостями и иждивенцами, вы обычно просто ввернуты.

Я столкнулся с проблемой циклических определений TH, которые невозможно до этого... Это довольно раздражает. Существует решение, но оно уродливо - обертывание вещей, связанных с циклической зависимостью в выражении TH, которое объединяет все сгенерированные объявления. Один из этих генераторов объявлений может быть просто квазициклером, который принимает код Haskell.

  • Это беспринципный. Я имею в виду, что большую часть времени, когда вы выражаете абстракцию, для этой абстракции есть какой-то принцип или понятие. Для многих абстракций принцип, лежащий в их основе, может быть выражен в их типах. Когда вы определяете класс типа, вы часто можете формулировать законы, которые должны подчиняться, и клиенты могут принять их. Если вы используете функцию GHC [new generics] [3], чтобы абстрагировать форму объявления экземпляра по любому типу данных (в пределах границ), вы можете сказать "для типов сумм, он работает так, для типов продуктов, он работает так". Но Template Haskell - это просто немые макросы. Это не абстракция на уровне идей, а абстракция на уровне АСТ, что лучше, но лишь скромно, чем абстракция на уровне обычного текста.

Это просто беспринципно, если вы делаете с ним беспринципные вещи. Единственное отличие заключается в том, что при компиляторе реализованы механизмы абстракции, у вас больше уверенности в том, что абстракция не является протекающей. Возможно, демократизация дизайна языка звучит немного страшно! Создатели библиотек TH должны хорошо документировать и четко определять смысл и результаты инструментов, которые они предоставляют. Хорошим примером принципиального TH является пакет вывода: http://hackage.haskell.org/package/derive - он использует DSL, так что пример многих дериваций/указывает/фактический вывод.

  • Он связывает вас с GHC. Теоретически другой компилятор мог бы его реализовать, но на практике я сомневаюсь, что это когда-нибудь произойдет. (Это противоречит разным типам системных расширений, которые, хотя они могут быть реализованы только GHC в настоящий момент, я легко мог представить, что их усыновили другие компиляторы по дороге и в конечном итоге стандартизированы.)

Это довольно хороший момент - TH API довольно большой и неуклюжий. Повторное внедрение кажется, что это может быть жестким. Тем не менее, существует всего лишь несколько способов разрезать проблему представления Haskell AST. Я предполагаю, что копирование TH ADT и запись конвертера во внутреннее представление AST обеспечили бы вам много пути. Это было бы эквивалентно (не несущественному) усилию создания haskell-src-meta. Его также можно было бы просто переделать, напечатав TH AST и используя внутренний синтаксический анализатор компилятора.

Хотя я мог ошибаться, я не вижу, чтобы TH был настолько сложным из расширения компилятора, с точки зрения реализации. Это на самом деле одно из преимуществ "поддержания его простоты" и отсутствия фундаментального слоя как теоретически привлекательной, статически проверяемой системы шаблонов.

  • API нестабилен. Когда новые функции языка добавляются в GHC, а пакет template-haskell обновляется для их поддержки, это часто включает в себя обратные несовместимые изменения типов данных TH. Если вы хотите, чтобы ваш код TH был совместим с более чем одной версией GHC, вам нужно быть очень осторожным и, возможно, использовать CPP.

Это также хороший момент, но несколько драматизированный. Несмотря на то, что в последнее время были добавлены API, они не подвергались экстенсивно обрыву. Кроме того, я думаю, что с вышеизложенным выше, упомянутым мной ранее, API, который действительно должен использоваться, может быть значительно сокращен. Если никакая конструкция/соответствие не нуждается в различных функциях и вместо этого выражаются как литералы, тогда большая часть API исчезает. Кроме того, код, который вы пишете, легче переносится в представления AST для языков, подобных Haskell.


В заключение, я думаю, что TH является мощным, полузабытым инструментом. Меньшая ненависть может привести к более оживленной экосистеме библиотек, поощряя внедрение более языковых прототипов. Было замечено, что TH является подавленным инструментом, который может позволить вам/делать/почти что угодно. Анархия! Что ж, это мое мнение, что эта сила может позволить вам преодолеть большую часть своих ограничений и построить системы, способные к совершенно принципиальным подходам метапрограммирования. Стоит использовать уродливые хаки для имитации "правильной" реализации, так как постепенно будет понятен дизайн "правильной" реализации.

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

Какой типичный ответ Haskell на шаблонный код? Абстракция. Какие наши любимые абстракции? Функции и классные классы!

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

  • Минимальные наборы привязок не подлежат декларабельности/компилятору. Это может привести к непреднамеренным определениям, которые дают дно из-за взаимной рекурсии.

  • Несмотря на большое удобство и мощь, которые могут уступить, вы не можете указывать значения суперкласса по умолчанию из-за сиротских экземпляров 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 привел к предпочтению к таким вещам, как метод-defaults, а не к более гибкому, макро-расширению, как декларации экземпляров. Дисциплина избегать вещей, которые могут привести к непредвиденным результатам, является разумной, однако мы не должны игнорировать, что система типа Haskell, способная обеспечить более надежное метапрограммирование, чем во многих других средах (путем проверки сгенерированного кода).

Ответ 5

Одна довольно прагматичная проблема с Template 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 даже работает, но точка все еще стоит).

Ответ 6

Почему это плохо? Для меня это сводится к следующему:

Если вам нужно создать так много повторяющегося кода, что вы пытаетесь использовать TH для его автоматической генерации, вы делаете это неправильно!

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

Иногда, конечно, это необходимо. Но иногда вы можете избежать необходимости в TH, просто будучи немного более умным с вашими проектами.

(Другое дело, что TH довольно низкоуровневый. Нет грандиозного дизайна на высоком уровне, много деталей внутренней реализации GHC раскрываются, и это заставляет API подвергать изменения...)