当我编写算法时,我通常会在注释中写下不变量。
例如,一个函数可能返回一个有序列表,而另一个函数期望列表是有序的。
我知道存在定理证明器,但我没有使用它们的经验。
我还相信聪明的编译器可以利用它们来优化程序。
因此,是否可以编写不变量并让编译器检查它们?
当我编写算法时,我通常会在注释中写下不变量。
例如,一个函数可能返回一个有序列表,而另一个函数期望列表是有序的。
我知道存在定理证明器,但我没有使用它们的经验。
我还相信聪明的编译器可以利用它们来优化程序。
因此,是否可以编写不变量并让编译器检查它们?
{-# LANGUAGE GADTs, PolyKinds, KindSignatures, MultiParamTypeClasses,
FlexibleInstances, RankNTypes, FlexibleContexts #-}
为了保持简单,我将使用自然数。
data Nat = Z | S Nat deriving (Show, Eq, Ord)
但是我将在Prolog类型类中定义<=
,这样类型检查器可以尝试隐式地确定顺序。
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
是该 family 的类型。 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)
我相应地扩展了“<=”的概念,这样类型检查器就可以进行边界检查。
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
这里是数字有序列表:一个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)
Nat
的 NATTY
。在Haskell中,Nat
、Natty
和NATTY
之间的相互作用有些令人沮丧,但现在就是这样。一旦我们得到了这个,我们就可以按照通常的分治方式构建 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
我经常感到惊讶的是,对我们有意义的许多程序也可以让类型检查器感到同样有意义。
[这是我建造的一些备用工具,用于观察发生了什么。
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))
并且没有任何内容被隐藏。
DataKinds
扩展。您还可以删除where
并简单地写成class LeN (m :: Nat) (n :: Nat)
,以及instance LeB Bot b
等。 - sdcvvc是的。
你可以在Haskell类型系统中编码你的不变量。编译器将执行类型检查等操作,以防止程序编译时不满足不变量。
对于有序列表,你可以考虑实现一个智能构造函数,在排序时改变列表的类型。
module Sorted (Sorted, sort) where
newtype Sorted a = Sorted { list :: [a] }
sort :: [a] -> Sorted a
sort = Sorted . List.sort
(!!) :: [a] -> b -> a
中,检查 b < length a
是否为真。 - Andrew(!!)
的任何地方时,都传递了一个索引,该索引在范围内。 Haskell代表了在静态和自动验证方面可以验证的相当甜蜜的点。 - Tom Crockett这里的其他答案都很棒,但是尽管您的问题特别提到了编译器检查,我觉得如果不至少提一下 QuickCheck,这个页面就会显得不完整。QuickCheck在运行时完成工作,而不是在编译时的类型系统中完成工作,但它是一个非常好的工具,用于测试可能在类型系统中静态表达过于困难或不方便的属性。