在Haskell中,我经常有一个像f
这样的函数,它接受一个列表并返回一个等长的列表:
f :: [a] -> [a] -- length f(xs) == length xs
同样,我可能有一个像g
这样的函数,它接受两个长度相等的列表:
g :: [a] -> [a] -> ...
如果按上述方式键入f
和g
,则如果不满足与长度相关的约束,则可能会导致运行时错误。因此,我想在类型系统中编码这些约束。我怎么能这样做?
请注意,我正在寻找可在日常情况下使用的实用框架,尽可能减少对代码的直观开销。我特别想知道你如何处理f
和g
你自己;也就是说,您是否会尝试将与长度相关的约束添加到其类型中,如此处所述,或者为了简化代码,您是否会使用上面给出的类型?
答案 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
(例如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]
答案 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这样的东西)。对于大多数有用的情况,这是真的......但是,有时你的实现不能被编译器“证明”,所以你必须在脑海中“证明”它,并使用某种不安全的强制