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

Manuel M T Chakravarty chak at justtesting.org
Tue Mar 27 22:50:22 UTC 2018


I can’t say that I am sure of the details, but Joachim’s proposal seems appealing to me.

Manuel

> Am 27.03.2018 um 16:24 schrieb Joachim Breitner <mail at joachim-breitner.de>:
> 
> 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/
> _______________________________________________
> ghc-steering-committee mailing list
> ghc-steering-committee at haskell.org
> https://mail.haskell.org/cgi-bin/mailman/listinfo/ghc-steering-committee

-------------- next part --------------
A non-text attachment was scrubbed...
Name: signature.asc
Type: application/pgp-signature
Size: 874 bytes
Desc: Message signed with OpenPGP
URL: <http://mail.haskell.org/pipermail/ghc-steering-committee/attachments/20180328/0e93a21f/attachment-0001.sig>


More information about the ghc-steering-committee mailing list