如果您考虑一下,您会发现您的类型listToChain
永远无法工作,因为它接受的值(b, Direction)
没有方向的类型级别信息,并且它仍然应该以某种方式找出方向索引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
and的链,nd
并试图证明它符合特定的pnu
and 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
并且我正在使用<$>
from 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
type 是 kind 的 value level 等价物,因此Nat
对于每种类型的 kindNat
都只有一个 type 值,SNat
这意味着即使删除了 type 的类型t
,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
允许它对任何类型都是通用的,让我们将其与Nat
s 一起使用)。因此,虽然proveNat
接受forall m. SNat m
如果我们可以在之后进行模式匹配Just Refl
,但我们(更重要的是,类型检查器)可以确定m
并且n
实际上是相同的类型。
的实例ProveNat
非常简单,但同样需要一些显式类型来帮助推理。
instance ProveNat Zero where
proveNat SZero = Just Refl
proveNat _ = Nothing
instance ProveNat n => ProveNat (Succ n) where
proveNat m@(SSucc _) = proveNat' m where
proveNat' :: forall p. ProveNat n => SNat (Succ p) -> Maybe (Refl (Succ p) (Succ n))
proveNat' (SSucc p) = case proveNat p :: Maybe (Refl p n) of
Just Refl -> Just Refl
_ -> Nothing
proveNat _ = Nothing