显然,通过一些GHC扩展,可以定义一种列表类型,其长度在类型中编码,如下所示:
{-# LANGUAGE GADTs, EmptyDataDecls #-}
data Z
data S a
data List l a where
Nil :: List Z a
Cons :: a -> List l a -> List (S l) a
虽然我知道为什么这会有用,但实际上却无法使用它。
如何创建这样的列表? (除了将其硬编码到程序中。)
假设有人想创建一个程序,从终端读取两个这样的列表并计算它们的点积。虽然很容易实现实际的乘法函数,但程序如何读取数据?
您能指出一些使用这些技术的现有代码吗?
答案 0 :(得分:3)
您不必硬编码列表的长度;相反,您可以定义以下类型:
data UList a where
UList :: Nat n => List n a -> UList a
,其中
class Nat n where
asInt :: n -> Int
instance Nat Z where
asInt _ = 0
instance Nat n => Nat (S n) where
asInt x = 1 + asInt (pred x)
where
pred = undefined :: S n -> n
我们也有
fromList :: [a] -> UList a
fromList [] = UList Nil
fromList (x:rest) =
case fromList rest of
UList xs -> UList (Cons x xs)
此设置允许您创建在编译时未知长度的列表;您可以通过执行case
模式匹配来从存在性包装器中提取类型来访问长度,然后使用Nat
类将类型转换为整数。
你可能想知道一个你不知道编译时的值的类型有什么好处;答案是,虽然您不知道类型是什么,但您仍然可以强制执行不变量。例如,以下代码保证不会更改列表的长度:
mapList :: (a -> b) -> List n a -> List n b
如果我们使用名为Add
的类型系列进行类型添加,那么我们可以编写
concatList :: List m a -> List n a -> List (Add m n) a
强制执行连接两个列表的不变量,获得一个包含两个长度之和的新列表。
答案 1 :(得分:2)
你几乎需要对它进行硬编码,因为类型当然是在编译时修复的,并且GHC类型检查器的图灵完整性不能被滥用来“自己”生成它们 1 < / SUP>。然而,这并不像听起来那么戏剧化:你基本上只需要编写一次长度注释类型。剩下的工作可以在不提及特定长度的情况下完成,尽管周围有一些奇怪的课程:
class LOL l where
lol :: [a] -> l a
instance LOL (List Z) where
lol _ = Nil
instance (LOL (List n)) => LOL (List (S n)) where
lol (x:xs) = Cons a $ lol xs
lol [] = error "Not enough elements given to make requested type length."
然后你可以使用像
这样的东西type Four = S(S(S(S Z)))
get4Vect :: Read a => IO (List Four a)
get4Vect = lol . read <$> getLine -- For input format [1,2,3,4].
1 我不会在这里讨论模板Haskell,它当然可以很容易地在编译时自动生成任何内容。
答案 2 :(得分:2)
长度编码在编译期间有效,因此显然类型检查器无法验证在运行时从例如用户输入。我们的想法是,您将任何运行时列表包装在隐藏长度参数的存在类型中,然后您必须提供有关长度的证明才能使用该列表。
例如:
{-# LANGUAGE GADTs #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Lists where
data Nat = Z | S Nat
data List l a where
Nil :: List Z a
Cons :: a -> List n a -> List (S n) a
data DynList a where
DynList :: List l a -> DynList a
data Refl a b where
Refl :: Refl a a
fromList :: [a] -> DynList a
fromList [] = DynList Nil
fromList (x:xs) = cons (fromList xs) where
cons (DynList rest) = DynList $ Cons x rest
toList :: List l a -> [a]
toList Nil = []
toList (Cons x xs) = x : toList xs
dot :: Num a => List l a -> List l a -> List l a
dot Nil Nil = Nil
dot (Cons x xs) (Cons y ys) = Cons (x*y) (dot xs ys)
haveSameLength :: List l a -> List l' b -> Maybe (Refl l l')
haveSameLength Nil Nil = Just Refl
haveSameLength (Cons _ xs) (Cons _ ys) = case haveSameLength xs ys of
Just Refl -> Just Refl
Nothing -> Nothing
haveSameLength _ _ = Nothing
main :: IO ()
main = do
dlx :: DynList Double <- fmap (fromList . read) getLine
dly :: DynList Double <- fmap (fromList . read) getLine
case (dlx, dly) of
(DynList xs, DynList ys) -> case haveSameLength xs ys of
Just Refl -> print $ toList $ dot xs ys
Nothing -> putStrLn "list lengths do not match"
这里DynList
是一个动态长度列表(即长度仅在运行时已知),它包含一个正确输入的List
。现在,我们有一个dot
函数来计算两个具有相同长度的列表的点积,所以如果我们像在示例中那样读取stdin中的列表,我们必须提供列表的证明,实际上,长度相同。
这里的“证明”是Refl
构造函数。声明构造函数的方式意味着如果我们可以提供类型为Refl a b
的值,那么a
和b
必须是同一类型。因此,我们使用hasSameLength
来验证生成的Refl
值的类型和模式匹配,并为类型检查器提供足够的信息,让我们可以在两个运行时调用dot
列表。
所以这实际上意味着类型检查器将强制我们手动验证编译时未知的任何列表的长度,以便编译代码。