如何构建具有依赖类型长度的列表?

时间:2014-11-20 00:19:09

标签: haskell dependent-type

将我的脚趾浸入依赖类型的水域中,我对规范的“静态类型长度列表”示例进行了修改。

{-# LANGUAGE DataKinds, GADTs, KindSignatures #-}

-- a kind declaration
data Nat = Z | S Nat

data SafeList :: (Nat -> * -> *) where
    Nil :: SafeList Z a
    Cons :: a -> SafeList n a -> SafeList (S n) a

-- the type signature ensures that the input list has at least one element
safeHead :: SafeList (S n) a -> a
safeHead (Cons x xs) = x

这似乎有效:

ghci> :t Cons 5 (Cons 3 Nil)
Cons 5 (Cons 3 Nil) :: Num a => SafeList ('S ('S 'Z)) a

ghci> safeHead (Cons 'x' (Cons 'c' Nil))
'x'

ghci> safeHead Nil
Couldn't match type 'Z with 'S n0
Expected type: SafeList ('S n0) a0
  Actual type: SafeList 'Z a0
In the first argument of `safeHead', namely `Nil'
In the expression: safeHead Nil
In an equation for `it': it = safeHead Nil

但是,为了使这个数据类型真正有用,我应该能够从运行时数据构建它,在编译时你不知道它的长度。我天真的尝试:

fromList :: [a] -> SafeList n a
fromList = foldr Cons Nil

无法编译,类型错误:

Couldn't match type 'Z with 'S n
Expected type: a -> SafeList n a -> SafeList n a
  Actual type: a -> SafeList n a -> SafeList ('S n) a
In the first argument of `foldr', namely `Cons'
In the expression: foldr Cons Nil
In an equation for `fromList': fromList = foldr Cons Nil

我理解为什么会发生这种情况:Cons的返回类型对于折叠的每次迭代都是不同的 - 这就是重点!但我无法看到解决方法,可能是因为我没有深入阅读这个主题。 (我无法想象所有这些努力都被放入一个在实践中无法使用的类型系统!)

那么:如何从“普通”简单类型数据中构建这种依赖类型的数据?


按照@ luqui的建议,我能够fromList编译:

data ASafeList a where
    ASafeList :: SafeList n a -> ASafeList a

fromList :: [a] -> ASafeList a
fromList = foldr f (ASafeList Nil)
    where f x (ASafeList xs) = ASafeList (Cons x xs)

这是我尝试解压缩ASafeList并使用它:

getSafeHead :: [a] -> a
getSafeHead xs = case fromList xs of ASafeList ys -> safeHead ys

这会导致另一种类型错误:

Couldn't match type `n' with 'S n0
  `n' is a rigid type variable bound by
      a pattern with constructor
        ASafeList :: forall a (n :: Nat). SafeList n a -> ASafeList a,
      in a case alternative
      at SafeList.hs:33:22
Expected type: SafeList ('S n0) a
  Actual type: SafeList n a
In the first argument of `safeHead', namely `ys'
In the expression: safeHead ys
In a case alternative: ASafeList ys -> safeHead ys

同样,直观地认为这将无法编译。我可以使用空列表调用fromList,因此编译器无法保证我能够在生成的safeHead上调用SafeList。缺乏知识大致是存在主义ASafeList捕获的。

这个问题可以解决吗?我觉得自己可能走在了一个合乎逻辑的死胡同。

3 个答案:

答案 0 :(得分:16)

永远不要扔掉任何东西。

如果你要麻烦地沿着一个列表来制作长度索引列表(在文献中称为“向量”),你也可以记住它的长度。

所以,我们有

data Nat = Z | S Nat

data Vec :: Nat -> * -> * where -- old habits die hard
  VNil :: Vec Z a
  VCons :: a -> Vec n a -> Vec (S n) a

但我们也可以给静态长度一个运行时表示。 Richard Eisenberg的“Singletons”软件包将为您完成此任务,但基本思路是为静态数字提供一种运行时表示。

data Natty :: Nat -> * where
  Zy :: Natty Z
  Sy :: Natty n -> Natty (S n)

至关重要的是,如果我们有Natty n类型的值,那么我们可以查询该值以找出n是什么。

Hasochists知道运行时可表示性通常很无聊,即使是机器也可以管理它,所以我们将它隐藏在类型类中

class NATTY (n :: Nat) where
  natty :: Natty n

instance NATTY Z where
  natty = Zy

instance NATTY n => NATTY (S n) where
  natty = Sy natty

现在,我们可以对您从列表中获得的长度进行更具信息性的存在性处理。

data LenList :: * -> * where
  LenList :: NATTY n => Vec n a -> LenList a

lenList :: [a] -> LenList a
lenList []        = LenList VNil
lenList (x : xs)  = case lenList xs of LenList ys -> LenList (VCons x ys)

您获得的代码与长度破坏版本相同,但您可以随时获取长度的运行时表示,并且您无需沿着向量爬行即可获取它。

当然,如果您希望长度为Nat,那么对某些Natty n而言,n仍然是一种痛苦。

将一个人的口袋弄得乱七八糟。

编辑我想我会添加一点,以解决“安全头”使用问题。

首先,让我为LenList添加一个解包器,它会为您提供手中的号码。

unLenList :: LenList a -> (forall n. Natty n -> Vec n a -> t) -> t
unLenList (LenList xs) k = k natty xs

现在假设我定义了

vhead :: Vec (S n) a -> a
vhead (VCons a _) = a

强制执行安全财产。如果我有一个向量长度的运行时表示,我可以查看它是否适用vhead

headOrBust :: LenList a -> Maybe a
headOrBust lla = unLenList lla $ \ n xs -> case n of
  Zy    -> Nothing
  Sy _  -> Just (vhead xs)

所以你要看一件事,这样做,了解另一件事。

答案 1 :(得分:5)

fromList :: [a] -> SafeList n a

n 普遍量化 - 即此签名声称我们应该能够从列表中构建任意长度的SafeList。相反,您希望量化存在,这只能通过定义新的数据类型来完成:

data ASafeList a where
    ASafeList :: SafeList n a -> ASafeList a

然后你的签名应该是

fromList :: [a] -> ASafeList a

您可以通过ASafeList

上的模式匹配来使用它
useList :: ASafeList a -> ...
useList (ASafeList xs) = ...

并且在正文中,xs将是SafeList n a类型,其中包含未知(刚性)n。您可能需要添加更多操作才能以任何不平凡的方式使用它。

答案 2 :(得分:1)

如果要在运行时数据上使用依赖类型的函数,则需要确保此数据不违反类型签名法中的编码。通过一个例子更容易理解这一点。这是我们的设置:

data Nat = Z | S Nat

data Natty (n :: Nat) where
    Zy :: Natty Z
    Sy :: Natty n -> Natty (S n)

data Vec :: * -> Nat -> * where
  VNil :: Vec a Z
  VCons :: a -> Vec a n -> Vec a (S n)

我们可以在Vec上编写一些简单的函数:

vhead :: Vec a (S n) -> a
vhead (VCons x xs) = x

vtoList :: Vec a n -> [a]
vtoList  VNil        = []
vtoList (VCons x xs) = x : vtoList xs

vlength :: Vec a n -> Natty n
vlength  VNil        = Zy
vlength (VCons x xs) = Sy (vlength xs)

为了编写lookup函数的规范示例,我们需要finite sets的概念。它们通常定义为

data Fin :: Nat -> where
    FZ :: Fin (S n)
    FS :: Fin n -> Fin (S n)

Fin n代表所有小于n的数字。

但就像类型级别相当于Nat s - Natty s一样,类型级别相当于Fin s。但现在我们可以合并价值级别和类型级别Fin s:

data Finny :: Nat -> Nat -> * where
    FZ :: Finny (S n) Z
    FS :: Finny n m -> Finny (S n) (S m)

第一个NatFinny的上限。第二个Nat对应于Finny的实际值。即它必须等于toNatFinny i,其中

toNatFinny :: Finny n m -> Nat
toNatFinny  FZ    = Z
toNatFinny (FS i) = S (toNatFinny i)

现在可以直接定义lookup函数:

vlookup :: Finny n m -> Vec a n -> a
vlookup  FZ    (VCons x xs) = x
vlookup (FS i) (VCons x xs) = vlookup i xs

还有一些测试:

print $ vlookup  FZ               (VCons 1 (VCons 2 (VCons 3 VNil))) -- 1
print $ vlookup (FS FZ)           (VCons 1 (VCons 2 (VCons 3 VNil))) -- 2
print $ vlookup (FS (FS (FS FZ))) (VCons 1 (VCons 2 (VCons 3 VNil))) -- compile-time error

这很简单,但take函数怎么样?这并不难:

type Finny0 n = Finny (S n)

vtake :: Finny0 n m -> Vec a n -> Vec a m
vtake  FZ     _           = VNil
vtake (FS i) (VCons x xs) = VCons x (vtake i xs)

我们需要Finny0而不是Finny,因为lookup要求Vec非空,所以如果类型为Finny n m的值,然后n = S n'用于某些n'。但是vtake FZ VNil完全有效,所以我们需要放宽这个限制。因此Finny0 n表示所有数字都小于或等于n

但是运行时数据呢?

vfromList :: [a] -> (forall n. Vec a n -> b) -> b
vfromList    []  f = f VNil
vfromList (x:xs) f = vfromList xs (f . VCons x)

即。 "给我一个列表和一个函数,它接受任意长度的Vec,并且我将后者应用于前者"。 vfromList xs返回一个继续(即(a -> r) -> r类型的东西)模数更高级别的类型。我们来试试吧:

vmhead :: Vec a n -> Maybe a
vmhead  VNil        = Nothing
vmhead (VCons x xs) = Just x

main = do
    print $ vfromList ([] :: [Int]) vmhead -- Nothing
    print $ vfromList  [1..5]       vmhead -- Just 1

作品。但我们不能重复自己吗?为什么vmhead,当vhead已经存在?我们是否应该以不安全的方式重写所有安全函数,以便可以在运行时数据上使用它们?那太傻了。

我们所需要的只是确保所有不变量都成立。让我们在vtake函数上尝试这个原则:

fromIntFinny :: Int -> (forall n m. Finny n m -> b) -> b
fromIntFinny 0 f = f FZ
fromIntFinny n f = fromIntFinny (n - 1) (f . FS)

main = do       
    xs <- readLn :: IO [Int]
    i <- read <$> getLine
    putStrLn $
        fromIntFinny i $ \i' ->
        vfromList xs   $ \xs' ->
        undefined -- what's here?

fromIntFinny就像vfromList一样。看看有哪些类型是有益的:

i'  :: Finny n m
xs' :: Vec a p

但是vtake有这种类型:Finny0 n m -> Vec a n -> Vec a m。因此,我们需要强制i',以使其类型为Finny0 p m。并且toNatFinny i'必须等于toNatFinny coerced_i'。但是这种强制一般是不可能的,因为如果S p < n,则Finny n m中的元素不在Finny (S p) m中,因为S pn是上限。

coerceFinnyBy :: Finny n m -> Natty p -> Maybe (Finny0 p m)
coerceFinnyBy  FZ     p     = Just FZ
coerceFinnyBy (FS i) (Sy p) = fmap FS $ i `coerceFinnyBy` p
coerceFinnyBy  _      _     = Nothing

这就是Maybe这里的原因。

main = do       
    xs <- readLn :: IO [Int]
    i <- read <$> getLine
    putStrLn $
        fromIntFinny i $ \i' ->
        vfromList xs   $ \xs' ->
        case i' `coerceFinnyBy` vlength xs' of
            Nothing  -> "What should I do with this input?"
            Just i'' -> show $ vtoList $ vtake i'' xs'

Nothing情况下,从输入中读取的数字大于列表的长度。在Just情况下,数字小于或等于列表的长度并强制转换为适当的类型,因此vtake i'' xs'的类型很好。

这很有效,但是我们引入了coerceFinnyBy函数,它看起来很特别。可判定的&#34;更少或相等&#34;关系将是适当的选择:

data (:<=) :: Nat -> Nat -> * where
    Z_le_Z :: Z :<= m                 -- forall n, 0 <= n
    S_le_S :: n :<= m -> S n :<= S m  -- forall n m, n <= m -> S n <= S m

type n :< m = S n :<= m

(<=?) :: Natty n -> Natty m -> Either (m :< n) (n :<= m) -- forall n m, n <= m || m < n
Zy   <=? m    = Right Z_le_Z
Sy n <=? Zy   = Left (S_le_S Z_le_Z)
Sy n <=? Sy m = either (Left . S_le_S) (Right . S_le_S) $ n <=? m

安全注射功能:

inject0Le :: Finny0 n p -> n :<= m -> Finny0 m p
inject0Le  FZ     _          = FZ
inject0Le (FS i) (S_le_S le) = FS (inject0Le i le)

即。如果n是某个数字和n <= m的上限,则m也是此数字的上限。还有一个:

injectLe0 :: Finny n p -> n :<= m -> Finny0 m p
injectLe0  FZ    (S_le_S le) = FZ
injectLe0 (FS i) (S_le_S le) = FS (injectLe0 i le)

现在代码如下:

getUpperBound :: Finny n m -> Natty n
getUpperBound = undefined

main = do
    xs <- readLn :: IO [Int]
    i <- read <$> getLine
    putStrLn $
        fromIntFinny i $ \i'  ->
        vfromList xs   $ \xs' ->
        case getUpperBound i' <=? vlength xs' of
            Left  _  -> "What should I do with this input?"
            Right le -> show $ vtoList $ vtake (injectLe0 i' le) xs'

它编译,但getUpperBound应该有什么定义?好吧,你无法定义它。 n中的Finny n m仅存在于类型级别,您无法提取或以某种方式获取。如果我们无法执行&#34; downcast&#34;,我们可以执行&#34; upcast&#34;:

fromIntNatty :: Int -> (forall n. Natty n -> b) -> b
fromIntNatty 0 f = f Zy
fromIntNatty n f = fromIntNatty (n - 1) (f . Sy)

fromNattyFinny0 :: Natty n -> (forall m. Finny0 n m -> b) -> b
fromNattyFinny0  Zy    f = f FZ
fromNattyFinny0 (Sy n) f = fromNattyFinny0 n (f . FS)

进行比较:

fromIntFinny :: Int -> (forall n m. Finny n m -> b) -> b
fromIntFinny 0 f = f FZ
fromIntFinny n f = fromIntFinny (n - 1) (f . FS)

因此,fromIntFinny中的延续对nm变量进行了普遍量化,而fromNattyFinny0中的延续通常仅针对m量化。 fromNattyFinny0收到Natty n而不是Int

Finny0 n m代替Finny n m,因为FZforall n m. Finny n m的元素,而FZ不一定是forall m. Finny n m的元素对于某些n,具体而言FZ不是forall m. Finny 0 m的元素(因此此类型无人居住)。

毕竟,我们可以加入fromIntNattyfromNattyFinny0

fromIntNattyFinny0 :: Int -> (forall n m. Natty n -> Finny0 n m -> b) -> b
fromIntNattyFinny0 n f = fromIntNatty n $ \n' -> fromNattyFinny0 n' (f n')

获得与@ pigworker的回答相同的结果:

unLenList :: LenList a -> (forall n. Natty n -> Vec n a -> t) -> t
unLenList (LenList xs) k = k natty xs

一些测试:

main = do
    xs <- readLn :: IO [Int]
    ns <- read <$> getLine
    forM_ ns $ \n -> putStrLn $
        fromIntNattyFinny0 n $ \n' i' ->
        vfromList xs         $ \xs'   ->
        case n' <=? vlength xs' of
            Left  _  -> "What should I do with this input?"
            Right le -> show $ vtoList $ vtake (inject0Le i' le) xs'

[1,2,3,4,5,6]
[0,2,5,6,7,10]

返回

[]
[1,2]
[1,2,3,4,5]
[1,2,3,4,5,6]
What should I do with this input?
What should I do with this input?

代码:http://ideone.com/3GX0hd

修改

  

嗯,你无法定义它。芬尼的一个n只生活在这种类型中   等级,你不能提取它或得到某种方式。

那不是真的。拥有SingI n => Finny n m -> ...后,我们可以将n设为fromSing sing