Задание инвариантов для конструкторов значений
Рассмотрим следующее
data Predicate = Pred Name Arity Arguments
type Name = String
type Arity = Int
type Arguments = [Entity]
type Entity = String
Это позволило бы создать
Pred "divides" 2 ["1", "2"]
Pred "between" 3 ["2", "1", "3"]
Но и "незаконный"
Pred "divides" 2 ["1"]
Pred "between" 3 ["2", "3"]
"незаконно", потому что arity не соответствует длине списка аргументов.
Если не использовать такую функцию, как эта
makePred :: Name -> Arity -> Arguments -> Maybe Predicate
makePred n a args | a == length args = Just (Pred n a args)
| otherwise = Nothing
И только экспортируя makePred из модуля предиката, можно ли обеспечить корректность конструктора значений?
2 ответа:
Ну, простой ответ состоит в том, чтобы отбросить arity от умного конструктора.
makePred :: Name -> Arguments -> Predicate makePred name args = Pred name (length args) args
Тогда, если вы не выставляете конструктор
Pred
из вашего модуля и не заставляете своих клиентов проходить черезmakePred
, вы знаете, что они всегда будут совпадать, и вам не нужно это неприглядноеMaybe
.Нетпрямого способа навязать этот инвариант. То есть вы не сможете получить
makePred 2 ["a","b"]
для проверки типа, ноmakePred 2 ["a","b","c"]
нет. Для этого вам нужны реальные зависимые типы.Есть места в середине, чтобы убедить Хаскелла применить ваши инварианты, используя расширенные функции (
GADT
S + phantom types), но после написания полного решения я понял, что на самом деле не обращался к вашему вопросу, и что такие методы не применимы к этой проблеме в частности. Они обычно доставляют больше хлопот, чем стоят в целом. Я бы остался с умным конструктором.Я написал подробное описание идеи интеллектуального конструктора. Оказывается ... быть довольно приятным промежуточным звеном между проверкой типа и проверкой времени выполнения.
Мне кажется, что если вы хотите, чтобы указанные ограничения были применимы, то вы должны сделать
Predicate
класс, и каждый вид предиката имеет свой собственный тип данных, который является экземпляромPredicate
.Это даст вам возможность иметь аргументы, отличные от строковых типов в ваших предикатах.
Что-то вроде этого (непроверенное)
data Entity = Str String | Numeric Int class Predicate a where name :: a -> String arity :: a -> Int args :: a -> [Entity] satisfied :: a -> Bool data Divides = Divides Int Int instance Predicate Divides where name p = "divides" arity p = 2 args (Divides n x) = [(Numeric n), (Numeric x)] satisfied (Divides n x) = x `mod` n == 0
Это может решить или не решить вашу проблему, но это, безусловно, сильный вариант для рассмотрения.