Haskell:在类型系统中指定列表的等长约束

sir*_*ton 9 haskell type-systems list dependent-type

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

f :: [a] -> [a]  -- length f(xs) == length xs
Run Code Online (Sandbox Code Playgroud)

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

g :: [a] -> [a] -> ...
Run Code Online (Sandbox Code Playgroud)

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

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

Sam*_*den 8

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

{-# 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
Run Code Online (Sandbox Code Playgroud)

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

万一这会(理解上)对你来说太复杂了,我不确定使用类型系统是否可行.最简单的方法是g使用monad/applicative(例如Maybe或者Either)进行定义,让我们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]
Run Code Online (Sandbox Code Playgroud)

  • 请注意,此定义在实际代码中实际上并不太实用,因为它禁止Functor,Appilcative,Traversable,Foldable,Monad等实例,这最终是处理类似这类类型的最有效方法,因为它们都提供了长度 - 保留或长度不可知的操作.此外,在现代haskell中,使用`DataKinds`来创建Nat*kind*也是首选,因为它可以防止一些无意义的`List`类型实例化. (2认同)
  • 为了澄清Justin L.的评论,您可以通过将其参数的顺序从"List an"更改为"List na"来改进您的类型. (2认同)

Jus*_* L. 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
Run Code Online (Sandbox Code Playgroud)

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

在第二种情况下,如果我们对结果的长度照顾,长度在编译时间,这是有意义的是已知的.

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

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