Расширение алгебраического типа данных

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

Учитывая тип данных, такой как Maybe:

data MyOwnMaybe a = MyOwnNothing | MyOwnJust a

все, кто использует мой тип данных, будут писать такие функции, как

maybeToList :: MyOwnMaybe a -> [a]
maybeToList MyOwnNothing  = []
maybeToList (MyOwnJust x) = [x]

Теперь предположим, что позднее я захочу расширить этот тип данных.

data MyOwnMaybe a = MyOwnNothing | MyOwnJust a | SuperpositionOfNothingAndJust a

как мне убедиться, что все функции сломаются во время компиляции?

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

data Action = Reset | Send | Remove

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


person Werner de Groot    schedule 30.08.2015    source источник
comment
Ваш вопрос действительно «убедиться, что все функции будут ломаться во время компиляции» или как предотвратить торможение чьего-либо кода? (Оба могут иметь смысл)   -  person leftaroundabout    schedule 30.08.2015
comment
похоже на небольшое изменение проблемы с выражениями   -  person Random Dev    schedule 30.08.2015
comment
Я думаю оба. Я действительно хочу убедиться, что программа (как часть, написанная мной, так и части, потребляющие мой код) будет функционировать так, как предполагается. Я чувствую, что рискую провалить многие из этих функций, которые (очевидно) не были написаны с учетом моего нового Add. Это падение вызовет, если я прав, исключение во время выполнения. Это было бы очень нежелательно.   -  person Werner de Groot    schedule 30.08.2015
comment
Как код должен вдруг узнать, как действовать, как предполагается, если вы добавите случаи?   -  person Random Dev    schedule 30.08.2015
comment
В этом случае я бы предпочел ошибку времени компиляции, чтобы знать, что мне нужно пересмотреть эти функции. Это отличалось бы от случая, когда у меня есть MaybeToList _ = [] в конце.   -  person Werner de Groot    schedule 30.08.2015


Ответы (3)


Ну, сначала плохие новости: иногда вы просто не можете этого сделать. Период.

Но это не зависит от языка; на любом языке иногда приходится ломать интерфейс. Нет никакого способа обойти это.

А теперь хорошие новости: вы действительно можете пройти большое расстояние, прежде чем вам придется это сделать.

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

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

Итак, в вашем примере, если вы пишете такие функции, как

myOwnNothing :: MyOwnMaybe a
myOwnJust :: a -> MyOwnMaybe a

и

fromMyOwnMaybe :: MyOwnMaybe a -> b -> (a -> b) -> b
fromMyOwnMaybe MyOwnNothing b _ = b
fromMyOwnMaybe (MyOwnJust a) _ f = f a

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

Единственная ситуация, в которой вам будет полезно экспортировать конструкторы, — это когда вы абсолютно уверены, что ваш тип данных никогда не изменится. Например, Bool всегда будет иметь только два (полностью определенных) значения: True и False, оно не будет расширено каким-либо FileNotFound или чем-то еще (хотя Эдвард Кметт может не согласиться). То же самое Maybe или [].

Но идея более общая: оставайтесь на как можно более высоком уровне.

person MigMit    schedule 31.08.2015
comment
Вы бы вообще посоветовали предоставлять доступ к типу данных x случаев с помощью функции, которая принимает x функций (некоторые из которых могут быть просто значениями по умолчанию, как вы сделали для MyOwnNothing), которые предоставляют доступ к этим x различным типам? - person Werner de Groot; 31.08.2015
comment
Это действительно зависит от предполагаемого использования. Я определил fromMyOwnMaybe, предполагая, что MyOwnMaybe будет служить почти той же цели, что и обычный Maybe. Как правило, вам нужно будет рассмотреть предполагаемый шаблон использования — что вы знаете о нем, что определенно должно быть, а что нет. Предустановленного набора правил нет. Если вы ничего не знаете, то да, этот fold подход может быть лучшим. - person MigMit; 01.09.2015

Вы, кажется, знаете, что GHC может предупреждать о неполных совпадениях шаблонов в функции с помощью флага -W или явно с помощью -fwarn-incomplete-patterns.

Существует хорошая дискуссия о том, почему эти предупреждения не являются автоматически ошибками времени компиляции в этом вопросе SO:

Почему в Haskell неполные шаблоны не ошибки времени компиляции?

Кроме того, рассмотрите случай, когда у вас есть АТД с большим количеством конструкторов:

data Alphabet = A | B | C | ... | X | Y | Z

isVowel :: Alphabet -> Bool
isVowel A = True
isVowel E = True
isVowel I = True
isVowel O = True
isVowel U = True
isVowel _ = False

Случай по умолчанию используется для удобства, чтобы не записывать остальные 21 случай.

Теперь, если вы добавите дополнительный конструктор в Alphabet, должен ли isVowel быть помечен как «незавершенный»?

person ErikR    schedule 30.08.2015
comment
Я этого не знал! Можно ли каким-то образом пометить функции, которые должны быть завершенными (без регистра по умолчанию), и отличить их от функций, которые будут работать независимо от того, добавим ли мы новые случаи или нет? (будь то через -W или каким-то другим способом) - person Werner de Groot; 30.08.2015
comment
@WernerdeGroot Не в Haskell, насколько мне известно. Мне действительно хотелось бы иметь возможность попросить компилятор рассматривать неполноту как ошибку. На некоторых других языках, например. Coq или Agda то есть по умолчанию. - person chi; 30.08.2015
comment
Вы можете сделать это с помощью -Werror, но это, очевидно, пакетная сделка. - person MasterMastic; 30.08.2015
comment
@MasterMastic Я считаю -Werror слишком радикальным, поскольку он влияет и на все остальные предупреждения. Хотя это вариант. Я хотел бы что-то, что я мог бы добавить в свой .cabal, не беспокоясь о том, что новые компиляторы будут слишком придирчивы в будущем. - person chi; 30.08.2015
comment
@чи, в этом есть смысл. Добавьте -ferror... к -fwarn... и -fno-warn.... - person dfeuer; 31.08.2015

Одна вещь, которую делают многие модули, — это не экспортировать свои конструкторы. Вместо этого они экспортируют функции, которые можно использовать (умные конструкторы). Если вы позже измените свой ADT, вам придется исправить свои функции в модуле, но ничей другой код не будет нарушен.

person PyRulez    schedule 31.08.2015