Осиротевшие экземпляры в Haskell

86

При компиляции моего приложения Haskell с -Wallопцией GHC жалуется на потерянные экземпляры, например:

Publisher.hs:45:9:
    Warning: orphan instance: instance ToSElem Result

Класс типа ToSElemне мой, он определен HStringTemplate .

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

Причина, по которой я хочу объявить свои ToSElemэкземпляры в модуле Publisher, заключается в том, что именно модуль Publisher зависит от HStringTemplate, а не другие модули. Я стараюсь разделять проблемы и избегать зависимости каждого модуля от HStringTemplate.

Я думал, что одно из преимуществ классов типов Haskell по сравнению, например, с интерфейсами Java, состоит в том, что они открыты, а не закрыты, и поэтому экземпляры не нужно объявлять в том же месте, что и тип данных. Похоже, что GHC советует игнорировать это.

Итак, то, что я ищу, - это либо какое-то подтверждение того, что мое мышление правильное и что я могу проигнорировать / подавить это предупреждение, либо более убедительный аргумент против того, чтобы поступать по-моему.

Дэн Дайер
источник
Обсуждение в ответах и ​​комментариях показывает, что существует большая разница между определением сиротских экземпляров в исполняемом файле , как вы это делаете, и в библиотеке , доступной другим. Этот чрезвычайно популярный вопрос показывает, насколько запутанными могут быть сиротские экземпляры для конечных пользователей библиотеки, которая их определяет.
Christian Conkle

Ответы:

94

Я понимаю, почему вы хотите это сделать, но, к сожалению, это может быть только иллюзией, что классы Haskell кажутся «открытыми» в том виде, в котором вы говорите. Многие люди считают, что такая возможность является ошибкой в ​​спецификации Haskell, по причинам, которые я объясню ниже. В любом случае, если это действительно не подходит для экземпляра, который вам нужно объявить либо в модуле, где объявлен класс, либо в модуле, где объявлен тип, это, вероятно, знак того, что вы должны использовать newtypeили какую-либо другую оболочку вокруг вашего типа.

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

Проблема проистекает из того факта, что когда существует более одного объявления экземпляра для одного и того же класса и типа, в стандартном Haskell нет механизма, чтобы указать, какой из них использовать. Скорее программа отклоняется компилятором.

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

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

Люди, которым нужны гарантии того, что эти проблемы никогда не возникнут с ними, должны следовать правилу, согласно которому, если кто-либо где-либо когда-либо объявил экземпляр определенного класса для определенного типа, никакой другой экземпляр никогда не должен быть объявлен снова в любой написанной программе. кем угодно. Конечно, есть обходной путь использования a newtypeдля объявления нового экземпляра, но это всегда как минимум незначительное неудобство, а иногда и серьезное. Так что в этом смысле те, кто намеренно пишет экземпляры-сироты, довольно невежливы.

Так что же делать с этой проблемой? Лагерь по борьбе с сиротскими экземплярами говорит, что предупреждение GHC - это ошибка, это должна быть ошибка, которая отклоняет любую попытку объявить сиротский экземпляр. А пока мы должны проявлять самодисциплину и избегать их любой ценой.

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

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

Из всего этого я пришел к выводу, что, по крайней мере, на данный момент, я настоятельно рекомендую вам избегать объявления каких-либо сиротских экземпляров, чтобы быть внимательным к другим, хотя бы по другой причине. Используйте файл newtype.

Иц
источник
4
В частности, это становится все более серьезной проблемой с ростом библиотек. При наличии> 2200 библиотек на Haskell и десятков тысяч отдельных модулей риск получения экземпляров резко возрастает.
Дон Стюарт
16
Re: «Я думаю, что правильным решением было бы добавить расширение к механизму импорта Haskell, которое бы контролировало импорт экземпляров». В случае, если эта идея кого-то заинтересует, возможно, стоит взглянуть на язык Scala в качестве примера; у него есть очень похожие функции для управления областью "неявных выражений", которые могут использоваться во многом как экземпляры классов типов.
Мэтт
5
Мое программное обеспечение - это приложение, а не библиотека, поэтому вероятность возникновения проблем у других разработчиков практически равна нулю. Вы можете рассматривать модуль Publisher как приложение, а остальные модули как библиотеку, но если бы я распространял библиотеку, то это было бы без Publisher и, следовательно, без осиротевших экземпляров. Но если бы я переместил экземпляры в другие модули, библиотека будет поставляться с ненужной зависимостью от HStringTemplate. В данном случае я думаю, что с сиротами все в порядке, но я прислушусь к вашему совету, если столкнусь с той же проблемой в другом контексте.
Дэн Дайер,
1
Звучит как разумный подход. Единственное, на что следует обратить внимание, - это если автор импортируемого модуля добавит этот экземпляр в более позднюю версию. Если этот экземпляр совпадает с вашим, вам нужно удалить собственное объявление экземпляра. Если этот экземпляр отличается от вашего, вам нужно будет окружить ваш тип оболочкой newtype, что может стать существенным рефакторингом вашего кода.
Yitz
@Matt: действительно, удивительно, что Scala понимает это именно так, а Haskell - нет! (кроме, конечно, Scala не хватает синтаксиса первого класса для машинного класса типов, что еще хуже ...)
Эрик Каплун,
44

Продолжайте и подавите это предупреждение!

Вы в хорошей компании. Конал делает это в "TypeCompose". "chp-mtl" и "chp-transformers" делают это, "control-monad-exception-mtl" и "control-monad-exception-monadsfd" делают это и т. д.

кстати, вы, вероятно, уже знаете это, но для тех, кто этого не делает и наткнулся на свой вопрос при поиске:

{-# OPTIONS_GHC -fno-warn-orphans #-}

Редактировать:

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

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

Немного отвлекает, но я считаю, что это идеальное решение в идеальном мире без компромиссов:

Я считаю, что проблемы, о которых упоминает Иц (не зная, какой экземпляр выбран), могут быть решены в "целостной" системе программирования, где:

  • Вы не редактируете простые текстовые файлы примитивно, а вам помогает среда (например, завершение кода предлагает только элементы соответствующих типов и т. Д.)
  • Язык «нижнего уровня» не имеет специальной поддержки для классов типов, вместо этого явно передаются таблицы функций.
  • Но среда программирования «более высокого уровня» отображает код аналогично тому, как сейчас представлен Haskell (обычно вы не увидите передаваемые таблицы функций), и выбирает для вас явные классы типов, когда они очевидны (для Например, во всех случаях Functor есть только один выбор), а когда есть несколько примеров (сжатый список Applicative или список-монада Applicative, First / Last / lift, возможно, Monoid), он позволяет вам выбрать, какой экземпляр использовать.
  • В любом случае, даже когда экземпляр был выбран для вас автоматически, среда легко позволяет вам увидеть, какой экземпляр был использован, с простым интерфейсом (гиперссылка, интерфейс наведения или что-то в этом роде)

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

Яирчу
источник
5
Да, но, возможно, каждое из этих происшествий является ошибкой определенного порядка. На ум приходят плохие экземпляры в control-monad-exception-mtl и monads-fd для Either. Было бы менее навязчиво, если бы каждый из этих модулей был вынужден определять свои собственные типы или предоставлять оболочки нового типа. Почти каждый сиротский экземпляр - это головная боль, ожидающая своего появления, и если ничего другого не потребует от вас постоянной бдительности, чтобы убедиться, что он импортирован или нет, как следует.
Эдвард КМЕТТ
2
Спасибо. Я думаю, что буду использовать их в этой конкретной ситуации, но благодаря Йитцу теперь я лучше понимаю, какие проблемы они могут вызвать.
Дэн Дайер,
37

Сиротские экземпляры - это неприятность, но, на мой взгляд, они иногда необходимы. Я часто комбинирую библиотеки, в которых тип взят из одной библиотеки, а класс - из другой. Конечно, нельзя ожидать, что авторы этих библиотек предоставят экземпляры для всех мыслимых сочетаний типов и классов. Так что я должен их обеспечивать, и они сироты.

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

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

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

август
источник
2
Хороший пример - (Ord k, Arbitrary k, Arbitrary v) ⇒ Arbitrary (Map k v)использование QuickCheck.
Эрик Каплун
17

В этом случае, я думаю, можно использовать сиротские экземпляры. Общее практическое правило для меня - вы можете определить экземпляр, если вы «владеете» классом типов или если вы «владеете» типом данных (или некоторым его компонентом, т. Е. Экземпляр для Maybe MyData тоже подойдет, по крайней мере, иногда). В рамках этих ограничений вы решаете разместить экземпляр - это ваше личное дело.

Есть еще одно исключение - если вы не владеете ни классом типов, ни типом данных, но создаете двоичный файл, а не библиотеку, то это тоже нормально.

sclv
источник
5

(Я знаю, что опаздываю на вечеринку, но это может быть полезно другим)

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

Тристан Спанглер
источник
3

В соответствии с этим, я понимаю, что такое положение библиотек WRT лагеря анти-сиротских экземпляров, но разве для исполняемых целей не должно хватить сиротских экземпляров?

mxc
источник
3
Что касается невежливости по отношению к другим, то вы правы. Но вы открываете себя для потенциальных будущих проблем, если этот же экземпляр когда-либо будет определен в будущем где-то в вашей цепочке зависимостей. Так что в этом случае вам решать, стоит ли рисковать.
Yitz
5
Почти во всех случаях реализации сиротского экземпляра в исполняемом файле он используется для заполнения пробела, который, по вашему желанию, уже был определен для вас. Поэтому, если экземпляр появляется в восходящем потоке, результирующая ошибка компиляции является просто полезным сигналом, сообщающим вам, что вы можете удалить свое объявление экземпляра.
Бен