LiquidHaskell

Ну что, пора пустить жиденького — как в «Терминаторе 2», где появляется робот из жидкого металла. Тип Int\mathtt{Int} гарантирует форму, но молчит о содержании: что число не ноль, что список не пуст, что индекс в границах. LiquidHaskell навешивает на обычные типы Haskell логические предикаты — уточняющие типы (refinement types) — и проверяет их при компиляции SMT-решателем (Z3). Аннотации-«декораторы» живут в специальных комментариях {-@    @-}\mathtt{\{\text{-}@\;\ldots\;@\text{-}\}}, поэтому код остаётся обычным Haskell и собирается обычным GHC. Синтаксис декораторов:

{v:τp(v)}\{\, v : \tau \mid p(v) \,\}
(2.1)

— «значения типа τ\tau, для которых верен предикат pp». Базовый пример — деление, которое невозможно вызвать с нулём:

{-@ type NonZero = {v:Int | v /= 0} @-}

{-@ safeDiv :: Int -> NonZero -> Int @-}
safeDiv :: Int -> Int -> Int
safeDiv x y = x `div` y

ok  = safeDiv 10 2    -- проходит: что 2 /= 0, решатель видит сам
bad = safeDiv 10 0    -- ошибка компиляции: 0 не влезает в NonZero

Под капотом это подтипирование — сравнение двух уточнённых типов, у каждого свой предикат: pp у аргумента, qq у параметра. Тип {v:τp}\{\, v:\tau \mid p \,\} — подтип {v:τq}\{\, v:\tau \mid q \,\}, когда любое значение, удовлетворяющее pp, удовлетворяет и qq, — эту импликацию и проверяет SMT-решатель. Для вызова safeDiv  10  2\mathtt{safeDiv}\;10\;2 у аргумента pp (v=2)\equiv (v = 2) — синглтонный тип литерала, у параметра NonZero\mathtt{NonZero}qq (v0)\equiv (v \neq 0), импликация vv =2= 2 v\Rightarrow v 0\neq 0 верна:

v.  p(v)q(v){v:τp}    {v:τq}\frac{\forall v.\; p(v) \Rightarrow q(v)}{\{\, v:\tau \mid p \,\} \;\preceq\; \{\, v:\tau \mid q \,\}}
(2.2)

Предикатам доступны меры (measures) — простые функции над данными, которые LiquidHaskell умеет разворачивать в логике. Классика — head\mathtt{head}, который перестаёт быть частичным:

{-@ measure len @-}
len :: [a] -> Int
len []     = 0
len (_:xs) = 1 + len xs

{-@ head' :: {v:[a] | len v > 0} -> a @-}
head' :: [a] -> a
head' (x:_) = x    -- ветка [] не нужна: она невозможна по типу

Рекурсию LiquidHaskell проверяет на завершимость — ищет убывающую метрику (по умолчанию — первый числовой аргумент). Тип Nat\mathtt{Nat} в сигнатуре — встроенный в LiquidHaskell алиас {v:Intv\{\, v:\mathtt{Int} \mid v 0}\geq 0 \,\}, объявленный так же, как наш NonZero\mathtt{NonZero}. Факториал из раздела про Y-комбинатор здесь тотален:

{-@ fac :: Nat -> Nat @-}
fac :: Int -> Int
fac 0 = 1
fac n = n * fac (n - 1)   -- n убывает и не уходит ниже нуля — решатель доволен

В режиме рефлексии на декораторах доказывают настоящие теоремы: свойство записывается типом, доказательство — программой. Это соответствие Карри — Ховарда:

{-@ LIQUID "--reflection" @-}
import Language.Haskell.Liquid.ProofCombinators

{-@ reflect double @-}
double :: Int -> Int
double x = x + x

{-@ doubleIsTwice :: x:Int -> { double x == 2 * x } @-}
doubleIsTwice :: Int -> Proof
doubleIsTwice x = double x === x + x === 2 * x *** QED

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