Подтвердить что ты не робот

Что плохого в OverlappingInstances?

Отвлекая некоторые недавние вопросы, я подумал, что я обращу внимание на старого пугателя, OverlappingInstances.

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

По пути я усвоил некоторую оценку точки зрения, что OverlappingInstances действительно не так чист и лучше избегается; главным образом, из-за того, что он не очень обоснован теоретически, в отличие от других больших расширений.

Но, думая об этом, я не уверен, могу ли я объяснить, что действительно так плохо об этом, другому человеку, если бы меня спросили.

Я ищу конкретные примеры того, как использование OverlappingInstances может привести к плохим вещам, будь то подрыв системы типов или других инвариантов или просто общая неожиданность или беспорядок.

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

Бонусный вопрос: до тех пор, пока мы находимся на предмет полезных, но не теоретически обоснованных расширений, которые могут привести к плохим событиям, почему GeneralizedNewtypeDeriving не получает такой же плохой рэп? Это потому, что отрицательные возможности легче локализовать; что легче увидеть, что вызовет проблемы и скажет: "Не делай этого"?

(Примечание: я бы предпочел, чтобы основная часть ответа была сосредоточена на OverlappingInstances, а не IncoherentInstances, которая нуждается в меньшем пояснении.)

EDIT: есть также хорошие ответы на аналогичный вопрос здесь.

4b9b3361

Ответ 1

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

К сожалению, это нарушено с OverlappingInstances. Например:

Модуль A:

{-# LANGUAGE FlexibleInstances, OverlappingInstances, MultiParamTypeClasses, FunctionalDependencies #-}

module A (Test(..)) where

class Test a b c | a b -> c where
   test :: a -> b -> c

instance Test String a String where
    test str _ = str

Модуль B:

module B where
import A (Test(test))

someFunc :: String -> Int -> String
someFunc = test

shouldEqualHello = someFunc "hello" 4

shouldEqualHello выполняет равенство "привет" в модуле B.

Теперь добавьте следующее объявление экземпляра в A:

instance Test String Int String where
    test s i = concat $ replicate i s

Было бы предпочтительнее, если бы это не повлияло на модуль B. Он работал до этого добавления и должен работать после этого. К сожалению, это не так.

Модуль B все еще компилируется, но теперь shouldEqualHello теперь равно "hellohellohellohello". Поведение изменилось, хотя ни один из методов, которые он изначально не использовал, изменился.

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

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