使用Data Kinds使用GADT动态构建值

时间:2014-05-04 00:39:19

标签: haskell gadt dependent-type data-kinds

为什么使用datakinds构建值更难,而与它们进行模式匹配相对容易?

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

data Nat = Zero | Succ Nat

data Direction = Center | Up | Down | UpDown deriving (Show, Eq)

data Chain :: Nat -> Nat -> * -> * where
    Nil    :: Chain Zero Zero a
    AddUp  :: a -> Chain nUp nDn a -> Chain (Succ nUp) nDn a
    AddDn  :: a -> Chain nUp nDn a -> Chain nUp (Succ nDn) a
    AddUD  :: a -> Chain nUp nDn a -> Chain (Succ nUp) (Succ nDn) a
    Add    :: a -> Chain nUp nDn a -> Chain nUp nDn a

lengthChain :: Num b => Chain (Succ Zero) (Succ Zero) a -> b
lengthChain = lengthChain'

lengthChain' :: forall (t::Nat) (t1::Nat) a b. Num b => Chain t t1 a -> b
lengthChain' Nil = 0
lengthChain' (Add   _ rest) = 1 + lengthChain' rest
lengthChain' (AddUp _ rest) = 1 + lengthChain' rest
lengthChain' (AddDn _ rest) = 1 + lengthChain' rest
lengthChain' (AddUD _ rest) = 1 + lengthChain' rest

chainToList :: Chain (Succ Zero) (Succ Zero) a -> [(a, Direction)]
chainToList = chainToList'

chainToList' :: forall (t::Nat) (t1::Nat) a. Chain t t1 a -> [(a, Direction)]
chainToList' Nil = []
chainToList' (Add a rest) = (a, Center):chainToList' rest
chainToList' (AddUp a rest) = (a, Up):chainToList' rest
chainToList' (AddDn a rest) = (a, Down):chainToList' rest
chainToList' (AddUD a rest) = (a, UpDown):chainToList' rest

listToChain :: forall (t::Nat) (t1::Nat) b. [(b, Direction)] -> Chain t t1 b
listToChain ((x, Center): xs) = Add x (listToChain xs)
listToChain ((x, Up):xs) = AddUp x (listToChain xs)
listToChain ((x, Down): xs) = AddDn x (listToChain xs)
listToChain ((x, UpDown): xs) = AddUD x (listToChain xs)
listToChain _ = Nil

我正在尝试构建一个数据类型来控制类似于列表的结构,不同之处在于我们可能会向元素添加箭头。此外,我要求某些功能仅在向上箭头和向下箭头的数量正好等于1的列表上运行。

在上面的代码中,函数listToChain无法编译,而chainToList正常编译。我们如何修复listToChain代码?

2 个答案:

答案 0 :(得分:6)

如果您稍微考虑一下,您会发现listToChain的类型无法正常工作,因为它会导致(b, Direction)的值没有< em>类型级别方向信息,它仍然应该以某种方式计算出编译时生成的Chain的方向索引类型。这显然是不可能的,因为在运行时,值可以由用户输入或从套接字等读取。

您需要跳过中间列表并直接从编译时验证值构建链,或者您可以将结果链包装在存在类型中并执行运行时检查以将存在性更新为更精确的类型

所以,给定一个像

这样的存在包装器
data SomeChain a where
    SomeChain :: Chain nu nd a -> SomeChain a

您可以将listToChain实施为

listToChain :: [(b, Direction)] -> SomeChain b
listToChain ((x, Center): xs) = withSome (SomeChain . Add x)   (listToChain xs)
listToChain ((x, Up):xs)      = withSome (SomeChain . AddUp x) (listToChain xs)
listToChain ((x, Down): xs)   = withSome (SomeChain . AddDn x) (listToChain xs)
listToChain ((x, UpDown): xs) = withSome (SomeChain . AddUD x) (listToChain xs)
listToChain _                 = SomeChain Nil

使用辅助函数withSome来更方便地包装和展开存在主义。

withSome :: (forall nu nd. Chain nu nd b -> r) -> SomeChain b -> r
withSome f (SomeChain c) = f c

现在我们有一个存在的东西,我们可以传递,隐藏精确的上下类型。当我们想要调用像lengthChain这样需要特定向上和向下计数的函数时,我们需要在运行时验证内容。一种方法是定义一个类型类。

class ChainProof pnu pnd where
    proveChain :: Chain nu nd b -> Maybe (Chain pnu pnd b)

proveChain函数采用任意nund的链,并尝试证明它符合特定的pnupnd。实现ChainProof需要一些重复的样板,但除了我们lengthChain所需的一个案例之外,它还可以为任何所需的起伏组合提供证据。

instance ChainProof Zero Zero where
    proveChain Nil          = Just Nil
    proveChain (Add a rest) = Add a <$> proveChain rest
    proveChain _            = Nothing

instance ChainProof u Zero => ChainProof (Succ u) Zero where
    proveChain (Add a rest)   = Add a   <$> proveChain rest
    proveChain (AddUp a rest) = AddUp a <$> proveChain rest
    proveChain _              = Nothing

instance ChainProof Zero d => ChainProof Zero (Succ d) where
    proveChain (Add a rest)   = Add a   <$> proveChain rest
    proveChain (AddDn a rest) = AddDn a <$> proveChain rest
    proveChain _              = Nothing

instance (ChainProof u (Succ d), ChainProof (Succ u) d, ChainProof u d) => ChainProof (Succ u) (Succ d) where
    proveChain (Add a rest)   = Add a   <$> proveChain rest
    proveChain (AddUp a rest) = AddUp a <$> proveChain rest
    proveChain (AddDn a rest) = AddDn a <$> proveChain rest
    proveChain (AddUD a rest) = AddUD a <$> proveChain rest
    proveChain _              = Nothing

以上内容需要语言扩展程序MultiParamTypeClassesFlexibleContexts以及我<$>使用Control.Applicative

现在我们可以使用证明机制为任何期望特定向上和向下计数的函数创建一个安全的包装器

safe :: ChainProof nu nd => (Chain nu nd b -> r) -> SomeChain b -> Maybe r
safe f = withSome (fmap f . proveChain)

这似乎是一个令人不满意的解决方案,因为我们仍然需要处理失败案例(即Nothing),但至少只需要在顶级检查。在给定的f内,我们对链的结构有静态保证,并且不需要进行任何额外的验证。

替代解决方案

上述解决方案虽然易于实现,但每次验证时都必须遍历并重新构建整个链。另一个选择是将上下计数存储为存在主义中的单身自然。

data SNat :: Nat -> * where
    SZero :: SNat Zero
    SSucc :: SNat n -> SNat (Succ n)

data SomeChain a where
    SomeChain :: SNat nu -> SNat nd -> Chain nu nd a -> SomeChain a

SNat类型相当于Nat种类的值等级,因此对于每种类型Nat,只有一个类型为SNat的值,这意味着即使t的{​​{1}}类型被删除,我们也可以通过对值进行模式匹配来完全恢复它。通过扩展,这意味着我们可以通过仅对自然进行模式匹配来恢复存在体中的完整类型SNat t,而不必遍历链本身。

构建链变得更加冗长

Chain

但另一方面,证据变得更短(虽然有点毛茸茸的类型签名)。

listToChain :: [(b, Direction)] -> SomeChain b
listToChain ((x, Center): xs) = case listToChain xs of
    SomeChain u d c -> SomeChain u d (Add x c)
listToChain ((x, Up):xs)      = case listToChain xs of
    SomeChain u d c -> SomeChain (SSucc u) d (AddUp x c)
listToChain ((x, Down): xs)   = case listToChain xs of
    SomeChain u d c -> SomeChain u (SSucc d) (AddDn x c)
listToChain ((x, UpDown): xs) = case listToChain xs of
    SomeChain u d c -> SomeChain (SSucc u) (SSucc d) (AddUD x c)
listToChain _                 = SomeChain SZero SZero Nil

这使用proveChain :: forall pnu pnd b. (ProveNat pnu, ProveNat pnd) => SomeChain b -> Maybe (Chain pnu pnd b) proveChain (SomeChain (u :: SNat u) (d :: SNat d) c) = case (proveNat u :: Maybe (Refl u pnu), proveNat d :: Maybe (Refl d pnd)) of (Just Refl, Just Refl) -> Just c _ -> Nothing 显式选择我们想要使用的ScopedTypeVariables的类型类实例。如果我们得到自然符合所请求值的证据,那么类型检查器很乐意让我们返回ProveNat而不进一步检查它。

Just c定义为

ProveNat

{-# LANGUAGE PolyKinds #-} data Refl a b where Refl :: Refl a a class ProveNat n where proveNat :: SNat m -> Maybe (Refl m n) 类型(反身性)是一种常用的模式,可以在我们在Refl构造函数上进行模式匹配时使类型检查器统一两种未知类型(并且Refl允许它任何类型的通用,让我们与PolyKinds s)一起使用。因此,如果Nat接受proveNat,如果我们之后可以在forall m. SNat m上进行匹配,那么我们(更重要的是类型检查器)可以确保Just Refl和{{1实际上是同一类型。

m的实例非常简单,但需要一些明确的类型来帮助推理。

n

答案 1 :(得分:4)

问题不在于datakinds。在类型

listToChain :: forall (t::Nat) (t1::Nat) b. [(b, Direction)] -> Chain t t1 b

您说任何类型t t1 b,您可以将b和路线对的列表转换为Chain t t1 b ...但这不是你的功能是对的,例如:

listToChain _ = Nil

此结果不适用于任何类型,但仅当t, t1同时为Zero时才有效。这就是GADT的重点,限制可能的类型。

我怀疑你想要为你的功能提供的类型是依赖,类似

listToChain :: (x :: [(b,Direction)]) -> Chain (number_of_ups x) (number_of_downs x) b

但是这在Haskell中不是有效的,因为Haskell没有依赖类型。一种解决方案是使用存在性

listToChain :: forall b. [(b, Direction)] -> exists (t :: Nat) (t1 :: Nat). Chain t t1 b

这是几乎有效的Haskell。不幸的是,存在感需要包含在构造函数中

data AChain b where
   AChain :: Chain t t1 b -> AChain b

然后你可以这样做:

listToChain :: forall b. [(b, Direction)] -> AChain b
listToChain ((x, Center): xs) = case (listToChain xs) of
   AChain y -> AChain (Add x y) 
...