How can I ‘convince’ GHC that I've excluded a certain case?

AJF*_*mar 6 haskell type-inference existential-type gadt

I have the following toy implementation of a non-empty list (NEList) datatype:

-- 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
Run Code Online (Sandbox Code Playgroud)

For example it's very easy to define a 'safe head' function that only operates on non-empty lists:

eHead :: NEList a -> a
eHead (Cons a _) = a
Run Code Online (Sandbox Code Playgroud)

The last is similarly easy, but has a little complication:

eLast :: NEList a -> a
eLast (Cons a Nil) = a
eLast (Cons _ b@(Cons _ _)) = eLast b
Run Code Online (Sandbox Code Playgroud)

The reason the pattern needs to be like this is to convince GHC that the type of b is indeed List 'NotEmpty, instead of an unknown existential type. The following code fails for that reason: (Couldn't match type ‘e’ with ‘'NotEmpty’ ...)

eLast :: NEList a -> a
eLast (Cons a Nil) = a
eLast (Cons _ b) = eLast b
Run Code Online (Sandbox Code Playgroud)

I'm fully aware why this is happening. What I'd like to know is, can I avoid having to write b@(Cons _ _) every time? Is there some other way I can restrict the type, so that GHC knows that b is referring strictly to something of type List 'NotEmpty?

One obvious way is to use unsafeCoerce but this defeats the point of the exercise.


For the sake of reproducibility, here is my preamble:

{-# OPTIONS_GHC -Wall -Werror #-} -- To prevent missed cases.
{-# LANGUAGE DataKinds        #-}
{-# LANGUAGE GADTs            #-}
{-# LANGUAGE KindSignatures   #-}
import Data.Kind
Run Code Online (Sandbox Code Playgroud)

Dan*_*ner 4

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

lastDef :: a -> List e a -> a
lastDef a Nil = a
lastDef _ (Cons a b) = lastDef a b
Run Code Online (Sandbox Code Playgroud)

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

last :: NEList a -> a
last (Cons a b) = lastDef a b
Run Code Online (Sandbox Code Playgroud)

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

  • @AJFarmar 我认为很难知道“Cons a Nil”不匹配意味着“Cons a (Cons b _)`匹配”是因为,在存在 GADT 的情况下,它并不总是显然,所有构造函数都是类型正确的可能性,因此简单地尝试尚未列出的所有构造函数是不正确的。例如,假设您的列表具有三个构造函数;其余两个模式中的哪一个类型等式(如果有的话)应该被引入到 `last (Cons a Nil) = a; 的右侧;最后(缺点 ab)= {- 这里 -}`? (2认同)