Осиротевшие экземпляры в Хаскеле
При компиляции моего приложения на Haskell с -Wall
вариант, GHC жалуется на осиротевшие экземпляры, например:
Publisher.hs:45:9:
Warning: orphan instance: instance ToSElem Result
Тип класса ToSElem
это не мое, это определяется HStringTemplate.
Теперь я знаю, как это исправить (перенести объявление экземпляра в модуль, где объявлен Result), и я знаю, почему GHC предпочел бы избегать осиротевших экземпляров, но я все еще считаю, что мой путь лучше. Мне все равно, если компилятор неудобен - скорее это, чем я.
Причина, по которой я хочу объявить ToSElem
экземпляры в модуле Publisher объясняются тем, что именно модуль Publisher зависит от HStringTemplate, а не от других модулей. Я пытаюсь сохранить разделение интересов и избежать зависимости каждого модуля от HStringTemplate.
Я подумал, что одним из преимуществ классов типов в Haskell по сравнению, например, с интерфейсами Java, является то, что они открыты, а не закрыты, и, следовательно, экземпляры не должны объявляться в том же месте, что и тип данных. Совет GHC, похоже, игнорирует это.
Итак, я ищу либо подтверждение того, что мое мышление обоснованно и что я буду оправдан в игнорировании / подавлении этого предупреждения, либо более убедительный аргумент против того, чтобы делать что-то по-своему.
6 ответов
Я понимаю, почему вы хотите сделать это, но, к сожалению, это может быть только иллюзией, что классы на Хаскеле кажутся "открытыми" в том смысле, как вы говорите. Многие люди считают, что возможность сделать это является ошибкой в спецификации Haskell, по причинам, которые я объясню ниже. В любом случае, если это действительно не подходит для экземпляра, вам нужно объявить либо в модуле, где объявлен класс, либо в модуле, где объявлен тип, это, вероятно, признак того, что вы должны использовать newtype
или какая-то другая обертка вокруг вашего типа.
Причины, по которым нужно избегать появления бесхозных экземпляров, намного глубже, чем удобство компилятора. Эта тема довольно противоречива, как вы можете видеть из других ответов. Чтобы уравновесить дискуссию, я собираюсь объяснить точку зрения о том, что никогда и никогда не следует писать бесхозные экземпляры, что, по моему мнению, является мнением большинства среди опытных хаскеллеров. Мое собственное мнение где-то посередине, которое я объясню в конце.
Проблема связана с тем фактом, что, когда существует несколько объявлений экземпляров для одного и того же класса и типа, в стандартном Haskell нет механизма, определяющего, какой из них использовать. Скорее программа отклонена компилятором.
Самый простой эффект этого - то, что у вас может быть отлично работающая программа, которая внезапно прекратит компиляцию из-за изменений, внесенных кем-то еще в какую-то удаленную зависимость вашего модуля.
Что еще хуже, работающая программа может начать сбой во время выполнения из-за отдаленного изменения. Возможно, вы используете метод, который, как вы предполагаете, происходит из определенного объявления экземпляра, и он может быть тихо заменен другим экземпляром, достаточно отличающимся для того, чтобы заставить вашу программу необъяснимым образом зависать.
Люди, которые хотят гарантировать, что эти проблемы никогда не возникнут, должны следовать правилу, согласно которому, если кто-либо где-либо когда-либо объявлял экземпляр определенного класса для определенного типа, ни один другой экземпляр никогда не должен объявляться снова в любой написанной программе. кем угодно. Конечно, есть обходной путь использования newtype
объявить новый экземпляр, но это всегда, по крайней мере, незначительное неудобство, а иногда и серьезное. Так что в этом смысле те, кто намеренно пишут сиротские экземпляры, довольно невежливы.
Так что же делать с этой проблемой? Лагерь анти-сироты говорит, что предупреждение GHC - это ошибка, это должна быть ошибка, которая отвергает любую попытку объявить экземпляр-сироту. Тем временем мы должны проявлять самодисциплину и избегать их любой ценой.
Как вы уже видели, есть те, кто не так обеспокоен этими потенциальными проблемами. Как вы предлагаете, они на самом деле поощряют использование бесхозных экземпляров в качестве инструмента для разделения интересов и говорят, что нужно просто в каждом конкретном случае убедиться, что проблем нет. Я был достаточно неудобен в случаях, когда другие люди осиротели, чтобы быть уверенным, что это отношение слишком кавалерное.
Я думаю, что правильным решением было бы добавить расширение к механизму импорта Haskell, который контролировал бы импорт экземпляров. Это не решило бы проблемы полностью, но помогло бы защитить наши программы от ущерба от бесхозных случаев, которые уже существуют в мире. And then, with time, I might become convinced that in certain limited cases, perhaps an orphan instance might not be so bad. (And that very temptation is the reason that some in the anti-orphan-instance camp are opposed to my proposal.)
My conclusion from all this is that at least for the time being, I would strongly advise that you avoid declaring any orphan instances, to be considerate to others if for no other reason. Использовать newtype
,
Давай, подавь это предупреждение!
Вы в хорошей компании. Конал делает это в "TypeCompose". "chp-mtl" и "chp-transformers" делают это, "control-monad-exception-mtl" и "control-monad-exception-monadsfd" делают это и т. д.
Кстати, вы, наверное, уже знаете это, но для тех, кто не знает и запутался ваш вопрос в поиске:
{-# OPTIONS_GHC -fno-warn-orphans #-}
Редактировать:
Я признаю проблемы, которые Иц упомянул в своем ответе, как реальные проблемы. Однако я вижу, что не использование осиротевших экземпляров также является проблемой, и я пытаюсь выбрать "наименьшее из всех зол", что является разумным для разумного использования осиротевших экземпляров.
В своем коротком ответе я использовал только восклицательный знак, потому что ваш вопрос показывает, что вы уже хорошо знаете о проблемах. Иначе я был бы менее восторженным:)
Немного отвлекает, но я считаю, что это идеальное решение в идеальном мире без компромиссов:
Я считаю, что проблемы, о которых упоминает Йитц (не зная, какой экземпляр выбран), могут быть решены в "целостной" системе программирования, где:
- Вы не редактируете простые текстовые файлы примитивно, а скорее оказывает помощь со стороны среды (например, завершение кода предлагает только вещи соответствующих типов и т. Д.)
- Язык "нижнего уровня" не имеет специальной поддержки для классов типов, и вместо этого таблицы функций передаются явно
- Но среда программирования "более высокого уровня" отображает код аналогично тому, как теперь представлен Haskell (вы обычно не видите переданные таблицы функций), и выбирает для вас явные классы типов, когда они очевидны (для Например, во всех случаях Functor есть только один выбор), и когда есть несколько примеров (почтовый список Applicative или list-monad Applicative, First / Last / lift, возможно, Monoid), он позволяет вам выбрать, какой экземпляр использовать.
- В любом случае, даже когда экземпляр был выбран для вас автоматически, среда легко позволяет вам увидеть, какой экземпляр был использован, с помощью простого интерфейса (гиперссылка или интерфейс наведения или что-то)
Вернувшись из мира фантазий (или, надеюсь, из будущего), прямо сейчас: я рекомендую стараться избегать сиротских случаев, все еще используя их, когда вам "действительно нужно"
Сиротские экземпляры - это неприятность, но, на мой взгляд, они иногда необходимы. Я часто комбинирую библиотеки, в которых тип происходит из одной библиотеки, а класс - из другой библиотеки. Конечно, нельзя ожидать, что авторы этих библиотек предоставят экземпляры для каждой мыслимой комбинации типов и классов. Поэтому я должен предоставить их, и поэтому они сироты.
Идея, что вам следует заключать тип в новый тип, когда вам нужно предоставить экземпляр, является идеей с теоретической точки зрения, но во многих случаях она слишком утомительна; Это идея, выдвинутая людьми, которые не пишут код на Haskell для жизни.:)
Так что давай давай сиротские экземпляры. Они безвредны.
Если вы можете аварийно завершить работу ghc с потерянными экземплярами, то это ошибка, о которой следует сообщить как таковой. (Ошибка, которую ghc имел / имеет о том, что не обнаруживает несколько экземпляров, не так сложно исправить.)
Но имейте в виду, что когда-нибудь в будущем кто-то другой может добавить такой экземпляр, как у вас уже есть, и вы можете получить ошибку (время компиляции).
В этом случае, я думаю, что использование экземпляров-сирот нормально. Основное правило для меня таково - вы можете определить экземпляр, если вы "владеете" классом типов или "владеете" типом данных (или каким-либо их компонентом), т. Е. Экземпляр Maybe MyData тоже подойдет, по крайней мере иногда). В рамках этих ограничений, когда вы решаете поместить экземпляр, это ваше личное дело.
Есть еще одно исключение - если вы не являетесь владельцем класса типов или типа данных, но создаете двоичный файл, а не библиотеку, то это тоже хорошо.
(Я знаю, что опаздываю на вечеринку, но это может быть полезно для других)
Вы можете оставить экземпляры-сироты в своем собственном модуле, и если кто-то импортирует этот модуль, то это именно потому, что он им нужен, и он может избежать их импорта, если он вызывает проблемы.
В связи с этим я понимаю, что WRT-библиотеки анти-сиротских лагерей располагаются, но для исполняемых целей разве не могут подойти сиротские экземпляры?