[ghc-steering-committee] Proposal: Binding existential type variables (#96)

Joachim Breitner mail at joachim-breitner.de
Tue Mar 27 05:24:51 UTC 2018


Hi,

I was close to writing “I am not convinced, but if I am the only one
who is unhappy with the current proposal, then I will not stand in the
way”. But I was away from the computer, so I could not write that, and
so I pondered the issue some more, and the more I ponder it, the more
dissatisfied with that outcome I am (and it kept me awake tonight…). 

But I want to be constructive, so I am offering an alternative
proposal. Still half-baked, of course, but hopefully already
understandable and useful.

### What are the issues?

But first allow me to summarize the points of contention, collecting
the examples that I brought up before, and adding some new ones.

1. It breaks equational reasoning.

      data T a where MkT :: forall a b. b -> T a
      foo (MkT @a y) = E[a :: y]
      bar = C[foo (MkT @Int True)]
   
   is different from

      bar = C[E[True :: Int]]

   While we can certainly teach people the difference between @ in 
   expression and @ in patterns, I find this a high price to pay.

2. The distinction between existentials and universals is oddly abrupt.

      data A a where MkA :: forall a. A a

   has a universal, but 

      data A' a where MkA' :: forall a. A (Id a)

   has an extensional.

   Also, every universal can be turned into an existential, by
   introducing a new universal. a is universal in A, but existential in
      

      data A'' a where MkA'' :: forall a b. a ~ b -> A b

   although it behaves pretty similar.

3. The intuition “existential = Output” and “universal = Input” is
   not as straight-forward as it first seems. I see how

      data B where MkB :: forall a. B

   has an existential that is unquestionably an output of the pattern 
   match. But what about

      data B2 where MkB2 :: forall a b. a ~ b -> B

   now if I pattern match (MkB2 @x @y), then yes, “x” is an output,
   but “y” somehow less so. Relatedly, if I write
   
      data B3 where MkB3 :: forall a. a ~ Int -> B

   then it’s hardly fair to say that (MkB3 @x) outputs a type “x”. And
   when we have 
   
      data B4 a where MkB4 :: forall a b. B (a,b)

   then writing (MkB4 @x @y) also does not really give us a new types
   stored in the constructor, but simply deconstructs the type argument
   of B4.

### Goals of the alternative proposal

So is there a way to have our cake and eat it too? A syntax that
 A. allows us to bind existential variables,
 B. has consistent syntax in patterns and expressions and
 C. has the desired property that every variable in a pattern is bound 
    there?

Maybe there is. The bullet point 3. above showed that thinking in terms
of Input and Output – while very suitable when we talk about the term
level – is not quite suitable on the Haskell type level, where
information flows in many ways and direction.

A better way of working with types is to think of type equalities,
unification and matching. And this leads me to 

### The alternative proposal (half-baked)

Syntax:
Constructors in pattern may have type arguments, using the @ syntax
from TypeApplication. The type argument correspond to _all_ the
universally quantified variables in their type signature.

Scoping:
Any type variable that occurs in a pattern is brought into scope by
this pattern. If a type variable occurs multiple times in the same
pattern, then it is the still same variable (no shadowing within a
pattern).

Semantics:
(This part is still a bit vague, if it does not make sense to you, then
better skip to the examples.)
Consider a constructor

  C :: forall a b. Ctx[a,b] => T (t2[a,b])

If we use use this in a pattern like so

  (C @s1 @s2)  (used at some type T t)

where s1 and s2 are types mentioning the type variables x and y, then
this brings x and y into scope. The compiler checks if the constraints
from the constructor C', namely
   (t ~ t2[a,b], Ctx[a,b])
imply that a and b have a shape that can be matched by s1 and s2.
If not, then a type error is reported.
If yes, then this matching yields instantiations for x and y.

Note that this description is completely independent of whether a or b
actually occur in t2[a,b]. This means that we can really treat all type
arguments of C in a consistent way, without having to put them into two
distinct categories.

One could summarize this idea as making these two changes to the
original proposal:
 + Every variable is an existential (in that proposal’s lingo)
 + We allow type patterns in the type applications in patterns, not
   just single variables.


### Examples

Let's see how this variant addresses the dubious examples above.

 * We now write 

      data T a where MkT :: forall a b. b -> T a
      foo (MkT @_ @a y) = E[a :: y]
      bar = C[foo (MkT @Int True)]
   
   which makes it clear that @a does not match the @Int, but rather an 
   implicit parameter. If the users chooses to be explicit and writes 
   it as
      bar = C[foo (MkT @Int @Bool True)]
   then equational reasoning worksand gives us   
      bar = C[E[True :: Bool]]

 * The difference between

      data A a  where MkA  :: forall a. A a
      data A' a where MkA' :: forall a. A (Id a)
      data A'' a where MkA'' :: forall a b. a ~ b -> A b

   disappears, as all behave identical in pattern matches:

      foo, foo', foo'' :: A Integer -> ()
      foo   (MkA   @x) = … here x ~ Integer  …
      foo'  (MkA'  @x) = … here x ~ Integer  …
      foo'' (MkA'' @x) = … here x ~ Integer  …

  * Looking at

      data B    where MkB  :: forall a. B
      data B2   where MkB2 :: forall a b. a ~ b -> B
      data B3   where MkB3 :: forall a. a ~ Int -> B
      data B4 a where MkB4 :: forall a b. B (a,b)

    we can write
      
      foo (MkB @x) = … x is bound …

    to match the existential, as before, and 

      foo (MkB @(Maybe x)) = 

    would fail, as expected. We can write 

      bar (MkB2 @x @y) = … x and y are in scope, and x~z …

    just as in the existing proposal, but it would also be legal to 
    write, should one desire to do so,

      bar (MkB2 @x @x) = 

 With B3 we can match this (fixed) existential, if we want. Or we
can
 assert it’s value:

   baz (MkB3 @x) = … x in scope, x ~ Int …
   baz (MkB3 @Int) = …

 With B4, this everything works just as expected:

  quux :: B4 ([Bool],())
  quux (MkB4 @x @y) = … x and y in scope, x ~ [Bool], y ~ ()

 which, incidentially, is equivalent to the existing

     quux (MkB4 :: (x, y)) = … x and y in scope, x ~ [Bool], y ~ ()

    and the new syntax also allows 

     quux' (MkB4 @[x]) = … x in scope, x ~ Bool
    

### A note on scoping

Above I wrote “every type variable in a pattern is bound there”. I fear
that this will be insufficient when people want to mention a type
variable bound further out. So, as a refinement of my proposal, maybe
the existing rules for Pattern type signatures
(https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html#pattern-type-signatures)
should apply: “a pattern type signature may mention a type variable
that is not in scope; in this case, the signature brings that type
variable into scope”

In this variant, all my babbling above can be understood as merely the
natural combination of the rules of ScopedTypeVariables (which brought
signatures to patterns) and TypeApplications (which can be understood
as an alternative syntax for type signatures).

What if I really want to introduce a new type variable x, even if x
might be in scope? Well, I could imagine syntax for that, where we can
list explicitly bound type variables for each function equation.
 
 data C a where MkC1 :: forall a, a -> C a
                MkC2 :: forall b, b -> C Int

 foo :: forall a. a -> C a -> ()
 foo x c = bar c
   where
      bar (MkC1 @a) = …
        -- matches the a bound by foo with the argument to MkC1
        -- which succeeds
      forall a. bar (MkC2 @a) = x
        -- introduces a fresh a, bound to MkC2’s existential argument
        -- and shadowing foo’s type variable a
      
 

Ok, that ended up quite long. I guess there is a strong risk that I am
describing something that Emmanuel, Simon and Richard have already
thought through and discarded… if so: why was it discarded? And if not:
does this proposal have merits?

Cheers and good night,
Joachim

PS: Happy 3rd anniversary to GHC-7.10 :-)


-- 
Joachim Breitner
  mail at joachim-breitner.de
  http://www.joachim-breitner.de/
-------------- next part --------------
A non-text attachment was scrubbed...
Name: signature.asc
Type: application/pgp-signature
Size: 833 bytes
Desc: This is a digitally signed message part
URL: <http://mail.haskell.org/pipermail/ghc-steering-committee/attachments/20180327/18aa307a/attachment.sig>


More information about the ghc-steering-committee mailing list