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

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

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

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

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

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

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

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


person Dan Dyer    schedule 20.06.2010    source источник
comment
Обсуждение в ответах и ​​комментариях показывает, что существует большая разница между определением сиротских экземпляров в исполняемом файле, как вы это делаете, и в библиотеке, доступной другим. . Этот чрезвычайно популярный вопрос показывает, насколько запутанными могут быть сиротские экземпляры для конечных пользователей библиотеки, которая их определяет.   -  person Christian Conkle    schedule 22.11.2014


Ответы (6)


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

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

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

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

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

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

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

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

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

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

person Yitz    schedule 20.06.2010
comment
В частности, это становится все более серьезной проблемой с ростом библиотек. Благодаря ›2200 библиотекам на Haskell и десяткам тысяч отдельных модулей риск получения экземпляров резко возрастает. - person Don Stewart; 20.06.2010
comment
Re: Я думаю, что правильным решением было бы добавить расширение к механизму импорта Haskell, которое бы контролировало импорт экземпляров. В случае, если эта идея кого-то заинтересует, возможно, стоит взглянуть на язык Scala в качестве примера; у него есть очень похожие функции для управления областью "неявных выражений", которые могут использоваться во многом как экземпляры классов типов. - person Matt; 20.06.2010
comment
Мое программное обеспечение - это приложение, а не библиотека, поэтому вероятность возникновения проблем у других разработчиков практически равна нулю. Вы можете рассматривать модуль Publisher как приложение, а остальные модули как библиотеку, но если бы я распространял библиотеку, то это было бы без Publisher и, следовательно, без осиротевших экземпляров. Но если бы я переместил экземпляры в другие модули, библиотека будет поставляться с ненужной зависимостью от HStringTemplate. В данном случае я думаю, что с сиротами все в порядке, но я прислушусь к вашему совету, если столкнусь с той же проблемой в другом контексте. - person Dan Dyer; 21.06.2010
comment
Звучит как разумный подход. Единственное, на что следует обратить внимание, - это если автор модуля, который вы импортируете, добавит этот экземпляр в более позднюю версию. Если этот экземпляр совпадает с вашим, вам необходимо удалить собственное объявление экземпляра. Если этот экземпляр отличается от вашего, вам нужно будет окружить ваш тип оболочкой newtype, что может стать существенным рефакторингом вашего кода. - person Yitz; 21.06.2010
comment
@Matt: действительно, удивительно, что Scala понимает это именно так, а Haskell - нет! (кроме, конечно, Scala не хватает синтаксиса первого класса для машинного класса типов, что еще хуже ...) - person Erik Kaplun; 16.05.2015

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

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

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

person yairchu    schedule 20.06.2010
comment
Да, но, возможно, каждое из этих происшествий является ошибкой определенного порядка. На ум приходят плохие экземпляры в control-monad-exception-mtl и monads-fd для Either. Было бы менее навязчиво, если бы каждый из этих модулей был вынужден определять свои собственные типы или предоставлять оболочки нового типа. Практически каждый сиротский экземпляр - это головная боль, ожидающая своего часа, и если ничего другого не потребуется, вам потребуется постоянная бдительность, чтобы убедиться, что он импортирован или нет. - person Edward KMETT; 20.06.2010
comment
Спасибо. Я думаю, что буду использовать их в этой конкретной ситуации, но благодаря Йитцу теперь я лучше понимаю, какие проблемы они могут вызвать. - person Dan Dyer; 21.06.2010

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

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

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

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

person augustss    schedule 21.06.2010
comment
Хороший пример (Ord k, Arbitrary k, Arbitrary v) ⇒ Arbitrary (Map k v) при использовании QuickCheck. - person Erik Kaplun; 16.05.2015

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

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

person sclv    schedule 20.06.2010

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

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

person Trystan Spangler    schedule 17.02.2011

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

person mxc    schedule 22.06.2010
comment
В том, что касается невежливости по отношению к другим, вы правы. Но вы открываете себя для потенциальных будущих проблем, если этот же экземпляр когда-либо будет определен в будущем где-то в вашей цепочке зависимостей. Так что в этом случае вам решать, стоит ли рисковать. - person Yitz; 27.06.2010
comment
Почти во всех случаях реализации сиротского экземпляра в исполняемом файле он используется для заполнения пробела, который вы хотите уже определили для вас. Поэтому, если экземпляр появляется в восходящем потоке, результирующая ошибка компиляции является просто полезным сигналом, сообщающим вам, что вы можете удалить свое объявление экземпляра. - person Ben; 07.07.2013