Можно ли запрограммировать и проверить инварианты в Haskell?

Когда я пишу алгоритм, я обычно записываю инварианты в комментариях.

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

Я также считаю, что интеллектуальный компилятор [sic!] может использовать их для оптимизации программы.
Итак, можно ли записать инварианты и заставить компилятор проверить их?

Ответ 1

Ну, ответ - да и нет. Невозможно просто написать инвариант отдельно от типа и проверить его. Однако было реализовано это в исследовательской вилке Haskell под названием ESC/Haskell: http://lambda-the-ultimate.org/node/1689

У вас есть другие варианты. Например, вы можете использовать утверждения: http://www.haskell.org/ghc/docs/7.0.2/html/users_guide/assertions.html

Затем с соответствующим флагом вы можете отключить эти утверждения для производства.

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

Еще один пример - это очень красивое кодирование красно-черных деревьев: http://www.reddit.com/r/haskell/comments/ti5il/redblack_trees_in_haskell_using_gadts_existential/

Ответ 2

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

{-# LANGUAGE GADTs, PolyKinds, KindSignatures, MultiParamTypeClasses,
    FlexibleInstances, RankNTypes, FlexibleContexts #-}

Я буду иметь натуральные числа, просто чтобы все было просто.

data Nat = Z | S Nat deriving (Show, Eq, Ord)

Но я определяю <= в типе класса Prolog, поэтому typechecker может попытаться сделать вывод порядка неявно.

class LeN (m :: Nat) (n :: Nat) where
instance             LeN Z n where
instance LeN m n =>  LeN (S m) (S n) where

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

data OWOTO :: Nat -> Nat -> * where
  LE :: LeN x y => OWOTO x y
  GE :: LeN y x => OWOTO x y

Мы хотели бы знать, что каждое два числа действительно упорядочиваются, если у нас есть представление о времени выполнения. В наши дни мы получаем это, построив одноэлементное семейство для Nat. Natty n - тип экземпляров времени выполнения n.

data Natty :: Nat -> * where
  Zy :: Natty Z
  Sy :: Natty n -> Natty (S n)

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

owoto :: forall m n. Natty m -> Natty n -> OWOTO m n
owoto Zy      n       = LE
owoto (Sy m)  Zy      = GE
owoto (Sy m)  (Sy n)  = case owoto m n of
  LE -> LE
  GE -> GE

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

data Bound x = Bot | Val x | Top deriving (Show, Eq, Ord)

Я расширяю понятие <= соответственно, поэтому typechecker может выполнять проверку границ.

class LeB (a :: Bound Nat)(b :: Bound Nat) where
instance             LeB Bot     b        where
instance LeN x y =>  LeB (Val x) (Val y)  where
instance             LeB (Val x) Top      where
instance             LeB Top     Top      where

И вот упорядоченные списки чисел: a OList l u - это последовательность x1 :< x2 :< ... :< xn :< ONil такая, что l <= x1 <= x2 <= ... <= xn <= u. x :< проверяет, что x находится над нижней границей, затем накладывает x как нижнюю границу на хвост.

data OList :: Bound Nat -> Bound Nat -> * where
  ONil :: LeB l u => OList l u
  (:<) :: forall l x u. LeB l (Val x) =>
          Natty x -> OList (Val x) u -> OList l u

Мы можем написать merge для упорядоченных списков так же, как если бы они были обычными. Ключевым инвариантом является то, что если оба списка имеют одинаковые границы, то и их слияние.

merge :: OList l u -> OList l u -> OList l u
merge ONil      lu         = lu
merge lu        ONil       = lu
merge (x :< xu) (y :< yu)  = case owoto x y of
  LE  -> x :< merge xu (y :< yu)
  GE  -> y :< merge (x :< xu) yu

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

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

data NATTY :: * where
  Nat :: Natty n -> NATTY

natty :: Nat -> NATTY
natty Z      =                           Nat Zy
natty (S n)  = case natty n of Nat n ->  Nat (Sy n)

Нам нужно верить, что этот перевод дает нам NATTY, который соответствует Nat, который мы хотим отсортировать. Это взаимодействие между Nat, NATTY и NATTY немного расстраивает, но это то, что требуется в Haskell только сейчас. Как только мы получим это, мы можем построить sort обычным способом "разделять и побеждать".

deal :: [x] -> ([x], [x])
deal []        = ([], [])
deal (x : xs)  = (x : zs, ys) where (ys, zs) = deal xs

sort :: [Nat] -> OList Bot Top
sort []   = ONil
sort [n]  = case natty n of Nat n -> n :< ONil
sort xs   = merge (sort ys) (sort zs) where (ys, zs) = deal xs

Я часто удивляюсь тому, сколько программ, которые имеют для нас смысл, могут иметь такой же смысл для typechecker.

[Вот какой запасной комплект я построил, чтобы посмотреть, что происходит.

instance Show (Natty n) where
  show Zy = "Zy"
  show (Sy n) = "(Sy " ++ show n ++ ")"
instance Show (OList l u) where
  show ONil = "ONil"
  show (x :< xs) = show x ++ " :< " ++ show xs
ni :: Int -> Nat
ni 0 = Z
ni x = S (ni (x - 1))

И ничего не было скрыто.]

Ответ 3

Да.

Вы кодируете свои инварианты в системе типа Haskell. Затем компилятор будет принудительно выполнять (например, выполнять проверку типов), чтобы предотвратить компиляцию вашей программы, если инварианты не удерживаются.

Для упорядоченных списков вы можете рассмотреть дешевый подход к реализации интеллектуального конструктора, который изменяет тип списка при сортировке.

module Sorted (Sorted, sort) where

newtype Sorted a = Sorted { list :: [a] }

sort :: [a] -> Sorted a
sort = Sorted . List.sort

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

Вы можете пойти гораздо дальше и кодировать чрезвычайно богатые свойства в систему типов. Примеры:

С практикой в ​​процессе компиляции язык может быть применен довольно сложными инвариантами.

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

Ответ 4

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