我该如何“说服”我已经排除了某种情况的GHC?

时间:2019-09-12 23:28:15

标签: haskell type-inference gadt existential-type

我具有以下非空列表(NEList)数据类型的玩具实现:

-- A type to describe whether or not a list is empty.
data Emptiness :: Type where
  Empty    :: Emptiness
  NotEmpty :: Emptiness

-- The list itself. Note the existential type in `Cons'.
data List :: Emptiness -> Type -> Type where
  Nil :: List 'Empty a
  Cons :: a -> List e a -> List 'NotEmpty a

type EList a = List 'Empty a
type NEList a = List 'NotEmpty a

例如,很容易定义仅对非空列表起作用的“安全头”功能:

eHead :: NEList a -> a
eHead (Cons a _) = a

最后一个同样容易,但有一点复杂:

eLast :: NEList a -> a
eLast (Cons a Nil) = a
eLast (Cons _ b@(Cons _ _)) = eLast b

该模式之所以需要这样的原因是为了使GHC确信b的类型确实是List 'NotEmpty,而不是未知的存在类型。由于以下原因,以下代码失败:(Couldn't match type ‘e’ with ‘'NotEmpty’ ...

eLast :: NEList a -> a
eLast (Cons a Nil) = a
eLast (Cons _ b) = eLast b

我完全知道为什么。我想知道的是,我是否可以避免每次都要写b@(Cons _ _)?有什么我可以限制类型的 other 方法,以便GHC知道b严格指的是List 'NotEmpty类型的东西?

一种明显的方法是使用unsafeCoerce,但这违反了练习的重点。


为了重现性,这是我的序言:

{-# OPTIONS_GHC -Wall -Werror #-} -- To prevent missed cases.
{-# LANGUAGE DataKinds        #-}
{-# LANGUAGE GADTs            #-}
{-# LANGUAGE KindSignatures   #-}
import Data.Kind

2 个答案:

答案 0 :(得分:4)

您可以做的一件事是传递空列表的替代方法:

lastDef :: a -> List e a -> a
lastDef a Nil = a
lastDef _ (Cons a b) = lastDef a b

然后在顶层将其包装一次。

last :: NEList a -> a
last (Cons a b) = lastDef a b

将此模式扩展到foldrfoldr1留给读者练习。

答案 1 :(得分:3)

您已经用NEList定义base的相同方式“定义”了NonEmpty(我以一种宽松的方式说):作为单个元素附加到可能为空的头部列表。

data NonEmpty a = a :| [a]

NonEmpty的另一种表示形式将单个元素放在末尾。

data NonEmpty a = Single a | Multiple a (NonEmpty a)

毫无疑问,此演示文稿使eLast变得容易:

eLast (Single x) = x
eLast (Multiple _ xs) = eLast xs

每当要在同一类型上使用多组构造函数时,请使用模式化同义词。除了基本的NonEmpty,我们还可以翻译为您的List

pattern Single :: forall e a. () => e ~ 'NotEmpty => a -> List e a
pattern Single x = Cons x Nil
pattern Multiple :: forall e a. () => e ~ 'NotEmpty => a -> List 'NotEmpty a -> List e a
pattern Multiple x xs <- Cons x xs@(Cons _ _)
  where Multiple x xs = Cons x xs
-- my dormant bidirectional pattern synonyms GHC proposal would allow just
-- pattern Multiple x (Cons y xs) = Cons x (Cons y xs)
-- but current pattern synonyms are a little stupid, so Multiple is ugly
{-# COMPLETE Nil, Single, Multiple :: List #-}
-- Single and Multiple are actually patterns on List e a, not List NotEmpty a
-- Therefore the COMPLETE must include Nil, or else we'd show that all
-- Lists are nonempty (\case { Single _ -> Refl; Multiple _ _ -> Refl })
-- They are, however, still complete on List NotEmpty a
-- GHC will "infer" this by "trying" the Nil constructor and deeming it impossible

给予

eLast :: NEList a -> a
eLast (Single x) = x
eLast (Multiple _ xs) = eLast xs