为什么使用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
代码?
答案 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
函数采用任意nu
和nd
的链,并尝试证明它符合特定的pnu
和pnd
。实现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
以上内容需要语言扩展程序MultiParamTypeClasses
和FlexibleContexts
以及我<$>
使用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)
...