Haskell:在类型系统

时间:2015-09-14 17:07:52

标签: list haskell type-systems dependent-type

在Haskell中,我经常有一个像f这样的函数,它接受一个列表并返回一个等长的列表:

f :: [a] -> [a]  -- length f(xs) == length xs

同样,我可能有一个像g这样的函数,它接受两个长度相等的列表:

g :: [a] -> [a] -> ...

如果按上述方式键入fg,则如果不满足与长度相关的约束,则可能会导致运行时错误。因此,我想在类型系统中编码这些约束。我怎么能这样做?

请注意,我正在寻找可在日常情况下使用的实用框架,尽可能减少对代码的直观开销。我特别想知道你如何处理fg你自己;也就是说,您是否会尝试将与长度相关的约束添加到其类型中,如此处所述,或者为了简化代码,您是否会使用上面给出的类型?

2 个答案:

答案 0 :(得分:7)

以下代码改编自Gabriel Gonzalez's blog,并结合评论中提供的一些信息:

{-# LANGUAGE GADTs, DataKinds #-}

data Nat = Z | S Nat

-- A List of length 'n' holding values of type 'a'
data List n a where
    Nil  :: List Z a
    Cons :: a -> List m a -> List (S m) a

-- Just for visualization (a better instance would work with read)
instance Show a => Show (List n a) where
    show Nil = "Nil"
    show (Cons x xs) = show x ++ "-" ++ show xs

g :: (a -> b -> c) -> List n a -> List n b -> List n c
g f (Cons x xs) (Cons y ys) = Cons (f x y) $ g f xs ys
g f Nil Nil = Nil

l1 = Cons 1 ( Cons 2 ( Nil ) ) :: List (S (S Z)) Int
l2 = Cons 3 ( Cons 4 ( Nil ) ) :: List (S (S Z)) Int
l3 = Cons 5 (Nil) :: List (S Z) Int

main :: IO ()
main = print $ g (+) l1 l2
       -- This causes GHC to throw an error:
       -- print $ g (+) l1 l3

此备用列表定义(使用GADT和DataKinds)对其类型中的列表长度进行编码。如果你定义了函数g :: List n a -> List n a -> ...,那么如果列表的长度不同,那么类型系统会抱怨。

如果这样(可以理解)对你来说太复杂了,我不确定使用类型系统是否可行。最简单的方法是使用monad / applicative定义g(例如MaybeEither),让g根据两个输入向列表中添加元素,并对结果进行排序。即。

g :: (a -> b -> c) -> [a] -> [b] -> Maybe [c]
g f l1 l2 = sequence $ g' f l1 l2
  where g' f (x:xs) (y:ys) = (Just $ f x y) : g' f xs ys
        g' f [] [] = []
        g' f _ _ = [Nothing]

main :: IO ()
main = do print $ g (+) [1,2] [2,3,4] -- Nothing
          print $ g (+) [1,2,3] [2,3,4] -- Just [3,5,7]

答案 1 :(得分:6)

您观察到的缺点是因为长度信息不是列表的类型的一部分;因为类型检查器是为了推理类型,所以你不能在函数中指定不变量,除非不变量在参数本身的类型中,或者在类型类约束上或键入基于家庭的平等。 (有一些haskell预处理器,比如Liquid Haskell,允许你用这样的不变量来注释函数,这些函数将在编译时检查。)

有许多haskell库提供类似列表的数据结构,其长度在类型中编码。一些值得注意的是线性(带有V)和固定向量

V的界面如下所示:

f :: V n a -> V n a -> V n a
g :: V n a -> V n a -> [a]
-- or --
g :: V n a -> V n a -> V ?? a -- if ?? can be determined at compile-time

请注意g的第一个类型签名的特定模式:我们在两个类型中关注长度,并返回关注长度的类型,丢失信息。

在第二种情况下,如果我们关心结果的长度,则必须在编译时知道长度才有意义。

请注意,线性的V实际上并不包含列表,而是包含矢量库中的Vector。它还需要镜头(线性库,即),如果你想要的只是长度编码的矢量,这无疑是一个很大的依赖。我认为来自 fixed-vectors 的矢量类型确实使用的东西更像是普通的haskell列表......但我并不完全确定。在任何情况下,它都有一个Foldable实例,因此您可以将其转换为列表。

当然要记住,如果你计划在这样的函数中编码长度...... Haskell / GHC必须能够看到你的实现类型检查!对于大多数这些库,Haskell将能够检测这样的事情(如果你坚持像压缩和fmapping,绑定,ap-ping这样的东西)。对于大多数有用的情况,这是真的......但是,有时你的实现不能被编译器“证明”,所以你必须在脑海中“证明”它,并使用某种不安全的强制