New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Or patterns #43

Open
wants to merge 60 commits into
base: master
from

Conversation

Projects
None yet
@osa1
Contributor

osa1 commented Jan 28, 2017

@cocreature

This comment has been minimized.

Show comment
Hide comment
@cocreature

cocreature Jan 28, 2017

I really like this idea and often wanted to have something like this.

I assume an or-pattern where different parts reference a different set of variables or the variables have different types will just result in an error message?

cocreature commented Jan 28, 2017

I really like this idea and often wanted to have something like this.

I assume an or-pattern where different parts reference a different set of variables or the variables have different types will just result in an error message?

@osa1

This comment has been minimized.

Show comment
Hide comment
@osa1

osa1 Jan 28, 2017

Contributor

I assume an or-pattern where different parts reference a different set of variables or the variables have different types will just result in an error message?

That's what I thought, but I think if we do something like:

for pattern in or_pattern:
  type check RHS against pattern

then we can allow different types for same variables in different patterns. Example:

data T a = C1 Int | C2 Bool

f :: T a -> String
f (C1 x | C2 x) = show x

If we type check show x for each pattern and only accept when for all patterns it typechecks I think we can accept this. Then during desugaring we can duplicate right-hand sides to get this:

f a = case a of
        C1 x -> show x
        C2 x -> show x

To keep things simple (and in within the parts of the compiler internals that I understand), I said "same set of variables of same types". We can either generalize this later or right now if type checker experts can chime in here.

One thing to note here is that is we restrict patterns to bind same set of variables of same types I think desugaring gets a lot simpler: we just generate a local function for the RHS, and call it with same set of variables in each case (similar to how fail_ functions generated by the desugarer). It gets more tricky as we generalize because we may have to pass typeclass dictionaries (and maybe some other things that I can't imagine right now).

Contributor

osa1 commented Jan 28, 2017

I assume an or-pattern where different parts reference a different set of variables or the variables have different types will just result in an error message?

That's what I thought, but I think if we do something like:

for pattern in or_pattern:
  type check RHS against pattern

then we can allow different types for same variables in different patterns. Example:

data T a = C1 Int | C2 Bool

f :: T a -> String
f (C1 x | C2 x) = show x

If we type check show x for each pattern and only accept when for all patterns it typechecks I think we can accept this. Then during desugaring we can duplicate right-hand sides to get this:

f a = case a of
        C1 x -> show x
        C2 x -> show x

To keep things simple (and in within the parts of the compiler internals that I understand), I said "same set of variables of same types". We can either generalize this later or right now if type checker experts can chime in here.

One thing to note here is that is we restrict patterns to bind same set of variables of same types I think desugaring gets a lot simpler: we just generate a local function for the RHS, and call it with same set of variables in each case (similar to how fail_ functions generated by the desugarer). It gets more tricky as we generalize because we may have to pass typeclass dictionaries (and maybe some other things that I can't imagine right now).

@mitchellwrosen

This comment has been minimized.

Show comment
Hide comment
@mitchellwrosen

mitchellwrosen Jan 28, 2017

One alternative to this feature is simply a warning for the existence of a wildcard pattern match.

mitchellwrosen commented Jan 28, 2017

One alternative to this feature is simply a warning for the existence of a wildcard pattern match.

@osa1

This comment has been minimized.

Show comment
Hide comment
@osa1

osa1 Jan 28, 2017

Contributor

One alternative to this feature is simply a warning for the existence of a wildcard pattern match.

What would be the fix for that warning? Without or patterns you just replace one problem with another (namely, wildcard patterns with repetitive and/or duplicated code).

Contributor

osa1 commented Jan 28, 2017

One alternative to this feature is simply a warning for the existence of a wildcard pattern match.

What would be the fix for that warning? Without or patterns you just replace one problem with another (namely, wildcard patterns with repetitive and/or duplicated code).

@mitchellwrosen

This comment has been minimized.

Show comment
Hide comment
@mitchellwrosen

mitchellwrosen Jan 28, 2017

Without or patterns you just replace one problem with another

That's true, but (IMO) it's replacing a big problem (wildcard patterns) with a small(er) one (duplicating the RHS on all the patterns you want to treat uniformly).

mitchellwrosen commented Jan 28, 2017

Without or patterns you just replace one problem with another

That's true, but (IMO) it's replacing a big problem (wildcard patterns) with a small(er) one (duplicating the RHS on all the patterns you want to treat uniformly).

@rwbarton

This comment has been minimized.

Show comment
Hide comment
@rwbarton

rwbarton Jan 28, 2017

I would very happy to have these. Ocaml has or-patterns already (https://caml.inria.fr/pub/docs/manual-ocaml/patterns.html#sec108), so we could borrow ideas from its implementation.

A real-world use case where or-patterns would be helpful that I mentioned just the other day to someone working on improving iOS support in GHC:

picCCOpts :: DynFlags -> [String]
picCCOpts dflags
    = case platformOS (targetPlatform dflags) of
      OSDarwin
          -- Apple prefers to do things the other way round.
          -- PIC is on by default.
          -- -mdynamic-no-pic:
          --     Turn off PIC code generation.
          -- -fno-common:
          --     Don't generate "common" symbols - these are unwanted
          --     in dynamic libraries.

       | gopt Opt_PIC dflags -> ["-fno-common", "-U__PIC__", "-D__PIC__"]
       | otherwise           -> ["-mdynamic-no-pic"]
      OSMinGW32 -- no -fPIC for Windows
       | -- ...

The OSiOS case needs to be the same as the OSDarwin case, and an or-pattern (OSDarwin | OSiOS) would be the most direct way to express that. (Otherwise you need to copy the whole OSDarwin case, or refactor the guards and bodies into a new binding and use it in both the OSDarwin and OSiOS cases. If the guards weren't "complete", so that there was a chance that no guard succeeds, then you'd need to duplicate the guards and bodies individually.)

As for syntax, I'm +1 on using | (as opposed to alternatives discussed in the ticket) because it is short, and already a reserved operator. There is a parse conflict with guards (exemplified in the above code snippet) but using parentheses to disambiguate is easy enough.

As for semantics when binding variables of different types, matching on existential constructors, etc., I'd be content with an incremental approach of supporting the simple cases first; that's where most of the value is anyways.

rwbarton commented Jan 28, 2017

I would very happy to have these. Ocaml has or-patterns already (https://caml.inria.fr/pub/docs/manual-ocaml/patterns.html#sec108), so we could borrow ideas from its implementation.

A real-world use case where or-patterns would be helpful that I mentioned just the other day to someone working on improving iOS support in GHC:

picCCOpts :: DynFlags -> [String]
picCCOpts dflags
    = case platformOS (targetPlatform dflags) of
      OSDarwin
          -- Apple prefers to do things the other way round.
          -- PIC is on by default.
          -- -mdynamic-no-pic:
          --     Turn off PIC code generation.
          -- -fno-common:
          --     Don't generate "common" symbols - these are unwanted
          --     in dynamic libraries.

       | gopt Opt_PIC dflags -> ["-fno-common", "-U__PIC__", "-D__PIC__"]
       | otherwise           -> ["-mdynamic-no-pic"]
      OSMinGW32 -- no -fPIC for Windows
       | -- ...

The OSiOS case needs to be the same as the OSDarwin case, and an or-pattern (OSDarwin | OSiOS) would be the most direct way to express that. (Otherwise you need to copy the whole OSDarwin case, or refactor the guards and bodies into a new binding and use it in both the OSDarwin and OSiOS cases. If the guards weren't "complete", so that there was a chance that no guard succeeds, then you'd need to duplicate the guards and bodies individually.)

As for syntax, I'm +1 on using | (as opposed to alternatives discussed in the ticket) because it is short, and already a reserved operator. There is a parse conflict with guards (exemplified in the above code snippet) but using parentheses to disambiguate is easy enough.

As for semantics when binding variables of different types, matching on existential constructors, etc., I'd be content with an incremental approach of supporting the simple cases first; that's where most of the value is anyways.

@Ericson2314

This comment has been minimized.

Show comment
Hide comment
@Ericson2314

Ericson2314 Jan 28, 2017

We should have done this along time ago. I use it all the time in Rust too, and miss it all the time in Haskell.

One interesting thing to note is that exhaustive lazy or patterns make sense.

Ericson2314 commented Jan 28, 2017

We should have done this along time ago. I use it all the time in Rust too, and miss it all the time in Haskell.

One interesting thing to note is that exhaustive lazy or patterns make sense.

@vagarenko

This comment has been minimized.

Show comment
Hide comment
@vagarenko

vagarenko Jan 29, 2017

Can I use it in case or lambda case:

stringOfT :: T -> Maybe String
stringOfT x = case x of
    T1 s -> Just s
    T2{} | T3{} -> Nothing

like this?

vagarenko commented Jan 29, 2017

Can I use it in case or lambda case:

stringOfT :: T -> Maybe String
stringOfT x = case x of
    T1 s -> Just s
    T2{} | T3{} -> Nothing

like this?

@rwbarton

This comment has been minimized.

Show comment
Hide comment
@rwbarton

rwbarton Jan 29, 2017

Can I use it in case or lambda case:

Yes although in this situation you would need parentheses around T2{} | T3{} (otherwise it parses as a guard).

An or-pattern is again a pattern so it can appear wherever a pattern can, including for example nested within another pattern (Just ('x' | 'y')).

rwbarton commented Jan 29, 2017

Can I use it in case or lambda case:

Yes although in this situation you would need parentheses around T2{} | T3{} (otherwise it parses as a guard).

An or-pattern is again a pattern so it can appear wherever a pattern can, including for example nested within another pattern (Just ('x' | 'y')).

@osa1

This comment has been minimized.

Show comment
Hide comment
@osa1

osa1 Jan 29, 2017

Contributor

@vagarenko, like @rwbarton said, an or pattern can appear anywhere that a pattern can appear.

OCaml already supports or patterns in full generality (i.e. they can appear anywhere that patterns can appear). This is from Real World OCaml:

let is_ocaml_source s =
  match String.rsplit2 s ~on:'.' with
  | Some (_,("ml"|"mli")) -> true
  | _ -> false

In Rust this is not the case, the reference says "Multiple match patterns may be joined with the | operator.".


I started thinking about the implementation. As the first thing I think we may have to make significant changes in the parser. Currently patterns are subsets of expressions, so we have productions like this:

pat     :  exp          {% checkPattern empty $1 }

checkPattern transforms an expression to a pattern:

checkPattern :: SDoc -> LHsExpr RdrName -> P (LPat RdrName)
checkPattern msg e = ...

With this change patterns won't be a subset of expressions, so we may want to first parse for a pattern, and then try to transform it into an expression. Does anyone have any other ideas on this?

Contributor

osa1 commented Jan 29, 2017

@vagarenko, like @rwbarton said, an or pattern can appear anywhere that a pattern can appear.

OCaml already supports or patterns in full generality (i.e. they can appear anywhere that patterns can appear). This is from Real World OCaml:

let is_ocaml_source s =
  match String.rsplit2 s ~on:'.' with
  | Some (_,("ml"|"mli")) -> true
  | _ -> false

In Rust this is not the case, the reference says "Multiple match patterns may be joined with the | operator.".


I started thinking about the implementation. As the first thing I think we may have to make significant changes in the parser. Currently patterns are subsets of expressions, so we have productions like this:

pat     :  exp          {% checkPattern empty $1 }

checkPattern transforms an expression to a pattern:

checkPattern :: SDoc -> LHsExpr RdrName -> P (LPat RdrName)
checkPattern msg e = ...

With this change patterns won't be a subset of expressions, so we may want to first parse for a pattern, and then try to transform it into an expression. Does anyone have any other ideas on this?

@rwbarton

This comment has been minimized.

Show comment
Hide comment
@rwbarton

rwbarton Jan 29, 2017

Pattern syntax was never really a subset of expression syntax (especially before TypeApplications):

Prelude> f @ x

<interactive>:2:1: Pattern syntax in expression context: f@x
Prelude> ~a

<interactive>:3:1: Pattern syntax in expression context: ~a

I'm not sure whether the pattern parser reuses the expression parser out of technical necessity (e.g., we don't know up front whether we are parsing a pattern or an expression) or out of convenience. If it's the latter it might be time to create a separate pattern parser. IIRC the reuse of the expression parser already causes some oddities around the precedence of @ and/or ~.

rwbarton commented Jan 29, 2017

Pattern syntax was never really a subset of expression syntax (especially before TypeApplications):

Prelude> f @ x

<interactive>:2:1: Pattern syntax in expression context: f@x
Prelude> ~a

<interactive>:3:1: Pattern syntax in expression context: ~a

I'm not sure whether the pattern parser reuses the expression parser out of technical necessity (e.g., we don't know up front whether we are parsing a pattern or an expression) or out of convenience. If it's the latter it might be time to create a separate pattern parser. IIRC the reuse of the expression parser already causes some oddities around the precedence of @ and/or ~.

@goldfirere

This comment has been minimized.

Show comment
Hide comment
@goldfirere

goldfirere Jan 31, 2017

Contributor

It's out of necessity. If a line begins f x y, the parser doesn't know if it's a naked top-level Template Haskell splice or the beginning of a function definition.

Naked top-level splices are a misfeature, in my opinion.

Contributor

goldfirere commented Jan 31, 2017

It's out of necessity. If a line begins f x y, the parser doesn't know if it's a naked top-level Template Haskell splice or the beginning of a function definition.

Naked top-level splices are a misfeature, in my opinion.

@saurabhnanda

This comment has been minimized.

Show comment
Hide comment
@saurabhnanda

saurabhnanda Jan 31, 2017

Is this proposal to force the pattern-match to match every possible ADT value explicitly? There are times where just having a wildcard _ match is a bug waiting to happen.

Actual use-case -- we start with the following ADT definition and call-sites:

data BookingStatus = Confirmed | Cancelled | Abandoned
computeRemainingSeats :: BookingStatus -> (...)
computeBilling :: BookingStatus -> (...)

Now, assume that within computeAvailability only Confirmed is explicitly matched to result in a reduction of available seats, everything else is matched by _ and results in a no-op. What happens when BookingStatus evolves to have a new value of ManualReview? We want to reduce the number of seats till the manual-review of the booking is complete. However, the compiler will not force us to look at this particular call-site to evaluate this impact.

Another one: assume that within computeBilling only Confirmed is explicitly matched to trigger an invoicing action, others are matched via _ to a no-op. What happens when BookingStatus evolves to have a new value of Refunded, which should trigger a credit-note action? Again, the compiler is not going to help us here.

Therefore, my question. Can we add a pragma to ADTs to disallow wildcards? eg.

data BookingStatus = Confirmed | Cancelled | Abandoned
{-# NoWildCardMatch BookingStatus #-}

Once this is done, having the or pattern will make explicit pattern matches easier to write.

saurabhnanda commented Jan 31, 2017

Is this proposal to force the pattern-match to match every possible ADT value explicitly? There are times where just having a wildcard _ match is a bug waiting to happen.

Actual use-case -- we start with the following ADT definition and call-sites:

data BookingStatus = Confirmed | Cancelled | Abandoned
computeRemainingSeats :: BookingStatus -> (...)
computeBilling :: BookingStatus -> (...)

Now, assume that within computeAvailability only Confirmed is explicitly matched to result in a reduction of available seats, everything else is matched by _ and results in a no-op. What happens when BookingStatus evolves to have a new value of ManualReview? We want to reduce the number of seats till the manual-review of the booking is complete. However, the compiler will not force us to look at this particular call-site to evaluate this impact.

Another one: assume that within computeBilling only Confirmed is explicitly matched to trigger an invoicing action, others are matched via _ to a no-op. What happens when BookingStatus evolves to have a new value of Refunded, which should trigger a credit-note action? Again, the compiler is not going to help us here.

Therefore, my question. Can we add a pragma to ADTs to disallow wildcards? eg.

data BookingStatus = Confirmed | Cancelled | Abandoned
{-# NoWildCardMatch BookingStatus #-}

Once this is done, having the or pattern will make explicit pattern matches easier to write.

@osa1

This comment has been minimized.

Show comment
Hide comment
@osa1

osa1 Jan 31, 2017

Contributor

Thanks for the examples @saurabhnanda. Your examples are exactly the same as the second example I gave in the proposal.

About the pragma: it sounds like a good idea, but it's orthogonal to this proposal and can be done separately. Even without or patterns it may be useful, so I suggest creating a new proposal for that.

So no, this is not a proposal to force pattern matching on every possible ADT constructor.

Contributor

osa1 commented Jan 31, 2017

Thanks for the examples @saurabhnanda. Your examples are exactly the same as the second example I gave in the proposal.

About the pragma: it sounds like a good idea, but it's orthogonal to this proposal and can be done separately. Even without or patterns it may be useful, so I suggest creating a new proposal for that.

So no, this is not a proposal to force pattern matching on every possible ADT constructor.

@saurabhnanda

This comment has been minimized.

Show comment
Hide comment

saurabhnanda commented Jan 31, 2017

@phadej

This comment has been minimized.

Show comment
Hide comment
@phadej

phadej Jan 31, 2017

Contributor

I don't really see the value of NoWildCardMatch pragma. Lazy programmer can workaround it by

isConfirmed :: BookingStatus -> Bool
isConfirmed Confirmed = True
isConfirmed Cancelled = False
isConfirmed Abandoned = False

andHereICanBeLazy :: BookingStatus -> IO ()
andHereICanBeLazy bs | isConfirmed = putStrLn "confirmed"
                     | otherwise   = putStrLn "not confirmed"

or in case of non-enums with YourType -> Maybe (Arg1, Arg2) or Prism.

I.e. at the end it will be about discipline in the team, so I don't see
it's good candidate for inclusion into GHC.

OTOH, feel free to experiment with writing hlint rules.
Maybe hlint doesn't support finding wildcard matches, but then experiment with haskell-src-exts,
it shouldn't be too difficult to find wildcard matches and make quick'n'dirty "type inference" (either from type signature, or other pattern match cases).

Contributor

phadej commented Jan 31, 2017

I don't really see the value of NoWildCardMatch pragma. Lazy programmer can workaround it by

isConfirmed :: BookingStatus -> Bool
isConfirmed Confirmed = True
isConfirmed Cancelled = False
isConfirmed Abandoned = False

andHereICanBeLazy :: BookingStatus -> IO ()
andHereICanBeLazy bs | isConfirmed = putStrLn "confirmed"
                     | otherwise   = putStrLn "not confirmed"

or in case of non-enums with YourType -> Maybe (Arg1, Arg2) or Prism.

I.e. at the end it will be about discipline in the team, so I don't see
it's good candidate for inclusion into GHC.

OTOH, feel free to experiment with writing hlint rules.
Maybe hlint doesn't support finding wildcard matches, but then experiment with haskell-src-exts,
it shouldn't be too difficult to find wildcard matches and make quick'n'dirty "type inference" (either from type signature, or other pattern match cases).

@peti

This comment has been minimized.

Show comment
Hide comment
@peti

peti Jan 31, 2017

Lazy programmer can workaround it by ...

It wouldn't call a programmer doing that "lazy". It's more like an hostile attacker actively trying to break the rules. Now, it's unfortunate that this issue exists, but it doesn't take away from the usefulness of NoWildCardMatch for those who want to be constructive about writing code that doesn't rely on catch-all default cases.

peti commented Jan 31, 2017

Lazy programmer can workaround it by ...

It wouldn't call a programmer doing that "lazy". It's more like an hostile attacker actively trying to break the rules. Now, it's unfortunate that this issue exists, but it doesn't take away from the usefulness of NoWildCardMatch for those who want to be constructive about writing code that doesn't rely on catch-all default cases.

@simonpj

This comment has been minimized.

Show comment
Hide comment
@simonpj

simonpj Jan 31, 2017

Contributor

I'm not against or-patterns, even mildly in favour.

But

we can allow different types for same variables in different patterns

Let's NOT do this . It would add a huge amount of complexity to a basically-simple feature. Each pattern in an or-group should bind the same term variables, exisitential type variables, and constraints.

Don't forget there is work to do to fix up the pattern-match overlap checker.

Contributor

simonpj commented Jan 31, 2017

I'm not against or-patterns, even mildly in favour.

But

we can allow different types for same variables in different patterns

Let's NOT do this . It would add a huge amount of complexity to a basically-simple feature. Each pattern in an or-group should bind the same term variables, exisitential type variables, and constraints.

Don't forget there is work to do to fix up the pattern-match overlap checker.

@qnikst

This comment has been minimized.

Show comment
Hide comment
@qnikst

qnikst Jan 31, 2017

starting from some level of complexity the good complex rule that is catch all but it may be changed in future, and workaround for bypassing the wildcard restriction feature are indistinguishable.
Also example provided by @phadej is completely OK with the NoWildcardMatch, because isConfirmed function uses it.
The only stable way of disallowing wildcard match without introducing bad side effects is to not expose internals but provide deconstructive:

data BookingStatus = Confirmed | NotConfirmed

withBookingStatus onConfirmed onNotConfirmed

Then with the change of the data type - the type of deconstructor will also change and all users will be notified.

qnikst commented Jan 31, 2017

starting from some level of complexity the good complex rule that is catch all but it may be changed in future, and workaround for bypassing the wildcard restriction feature are indistinguishable.
Also example provided by @phadej is completely OK with the NoWildcardMatch, because isConfirmed function uses it.
The only stable way of disallowing wildcard match without introducing bad side effects is to not expose internals but provide deconstructive:

data BookingStatus = Confirmed | NotConfirmed

withBookingStatus onConfirmed onNotConfirmed

Then with the change of the data type - the type of deconstructor will also change and all users will be notified.

@simonmar

This comment has been minimized.

Show comment
Hide comment
@simonmar

simonmar Jan 31, 2017

It's out of necessity. If a line begins f x y, the parser doesn't know if it's a naked top-level Template Haskell splice or the beginning of a function definition.

it was a necessity even before top-level naked splices, e.g. in the statements of do or a list comprehension there's a clash between p <- e (a bind) and just e (a guard).

simonmar commented Jan 31, 2017

It's out of necessity. If a line begins f x y, the parser doesn't know if it's a naked top-level Template Haskell splice or the beginning of a function definition.

it was a necessity even before top-level naked splices, e.g. in the statements of do or a list comprehension there's a clash between p <- e (a bind) and just e (a guard).

@simonpj

This comment has been minimized.

Show comment
Hide comment
@simonpj

simonpj Jul 5, 2018

Contributor

Yes, that's what Omer's proposal says, but note "implies". The specification is that (p1 ; p2) typechecks precisely when its desugaring

(\x-> case x of p1 -> Just (v1,..,vn); p2 -> Just (v1,..,vn); _ -> Nothing) ->Just (v1,..,vn))

Stuff about existentials etc is all a consequence of this rule. The rule does allow some or-patterns involving GADTs to typecheck; and it (importantly) it specifies precisely which ones.

Contributor

simonpj commented Jul 5, 2018

Yes, that's what Omer's proposal says, but note "implies". The specification is that (p1 ; p2) typechecks precisely when its desugaring

(\x-> case x of p1 -> Just (v1,..,vn); p2 -> Just (v1,..,vn); _ -> Nothing) ->Just (v1,..,vn))

Stuff about existentials etc is all a consequence of this rule. The rule does allow some or-patterns involving GADTs to typecheck; and it (importantly) it specifies precisely which ones.

@gasche

This comment has been minimized.

Show comment
Hide comment
@gasche

gasche Jul 5, 2018

It's all in the eye of the beholder but I don't personally think that this desugaring (elaboration) is the best specification device. It is perfect for implementation (and thus makes it very easy to test the proposal, even during the prototyping stages), but not for specification:

  • The specification is this desugaring rule plus a specification of view patterns (ViewPatterns#Semantics looks suitable, and is fairly simple).

  • If you composed both together, you would get a more direct static semantics as a fairly simple typing rule, and you would get the opportunity to talk about exhaustivity, etc., which are out of reach of general view patterns.

  • Finally, as far as I can tell, there is no easy extension of the specification through view patterns to export constraints or existential type variables. You would have to change the specification of view patterns to be in CPS style, I think, or to locally define a new GADT to export the existential variables and constraints as data. All these would also require changing the desugaring scheme -- the split between or-patterns and view patterns is not modular. On the contrary, a direct typing rule would be easy to extend in this way -- the pain is more on the implementation side, checking whether constraints are equal.

P.S.: This is not a criticism of Ömer's specification. I think that the couple sentences I quoted above can be taken as an "equivalent specification" of the impact of proposal for GADTs. The two specifications (the desugaring and its implication) are equivalent in terms of understanding ors-and-GADTs, it doesn't really matter what is ground truth and what is consequence.

gasche commented Jul 5, 2018

It's all in the eye of the beholder but I don't personally think that this desugaring (elaboration) is the best specification device. It is perfect for implementation (and thus makes it very easy to test the proposal, even during the prototyping stages), but not for specification:

  • The specification is this desugaring rule plus a specification of view patterns (ViewPatterns#Semantics looks suitable, and is fairly simple).

  • If you composed both together, you would get a more direct static semantics as a fairly simple typing rule, and you would get the opportunity to talk about exhaustivity, etc., which are out of reach of general view patterns.

  • Finally, as far as I can tell, there is no easy extension of the specification through view patterns to export constraints or existential type variables. You would have to change the specification of view patterns to be in CPS style, I think, or to locally define a new GADT to export the existential variables and constraints as data. All these would also require changing the desugaring scheme -- the split between or-patterns and view patterns is not modular. On the contrary, a direct typing rule would be easy to extend in this way -- the pain is more on the implementation side, checking whether constraints are equal.

P.S.: This is not a criticism of Ömer's specification. I think that the couple sentences I quoted above can be taken as an "equivalent specification" of the impact of proposal for GADTs. The two specifications (the desugaring and its implication) are equivalent in terms of understanding ors-and-GADTs, it doesn't really matter what is ground truth and what is consequence.

@nomeata

This comment has been minimized.

Show comment
Hide comment
@nomeata

nomeata Jul 5, 2018

Contributor

Has anyone already tried to give a more high-level specification in terms of typing rules like the ones in the Type variables in patterns paper? Maybe even a more ambitious specification that exports constraints or existential type variables? (It seems it would need a notion of “the intersection of two contexts”…)

Contributor

nomeata commented Jul 5, 2018

Has anyone already tried to give a more high-level specification in terms of typing rules like the ones in the Type variables in patterns paper? Maybe even a more ambitious specification that exports constraints or existential type variables? (It seems it would need a notion of “the intersection of two contexts”…)

@osa1

This comment has been minimized.

Show comment
Hide comment
@osa1

osa1 Jul 5, 2018

Contributor

I agree with @gasche on these points:

  • Giving static semantics via a desugaring to view patterns means we can't talk about exhaustiveness checks.

  • Because this desugaring rule can't be extended to handle GADTs in the next iteration we'll have to redefine or patterns again. (I don't intend to work on another iteration but I think someone else will surely work on this)

In addition to the first point, we can't really implement or patterns as this desugaring because exhaustiveness check would bail out even on simplest or patterns. This would be unacceptable as the whole point here is to make programs easier to refactor and reduce repetition. But if we implement it in any other way that the proposal won't be accurate anymore; the actual static semantics (when we include exhaustiveness check in the static semantics) will be different than the proposed one.

So I suggest this:

  • Give one additional rule to "Type variables in patterns"'s Figure 4 as the typing rule. No constraints will be in the type environment when checking the RHS, and universals will be renamed as in PatCon98.

  • Revert the dynamic semantics section, use the one from the previous revision.

  • Talk briefly about the exhaustiveness check. I think simply saying that or patterns will be checked as if the alternative is expanded into two alternatives before checking would be enough.

This thread became impossible to follow so I'll include a changelog section in the proposal, talking briefly about what designs and we considered in this thread. This'll also help readers in the future to see what we considered but discarded.

@simonpj @goldfirere @nomeata could any of you share the .tex source of the paper so that I could extend Figure 4 with one more rule for or patterns?

@mchakravarty let's not merge this yet.

Contributor

osa1 commented Jul 5, 2018

I agree with @gasche on these points:

  • Giving static semantics via a desugaring to view patterns means we can't talk about exhaustiveness checks.

  • Because this desugaring rule can't be extended to handle GADTs in the next iteration we'll have to redefine or patterns again. (I don't intend to work on another iteration but I think someone else will surely work on this)

In addition to the first point, we can't really implement or patterns as this desugaring because exhaustiveness check would bail out even on simplest or patterns. This would be unacceptable as the whole point here is to make programs easier to refactor and reduce repetition. But if we implement it in any other way that the proposal won't be accurate anymore; the actual static semantics (when we include exhaustiveness check in the static semantics) will be different than the proposed one.

So I suggest this:

  • Give one additional rule to "Type variables in patterns"'s Figure 4 as the typing rule. No constraints will be in the type environment when checking the RHS, and universals will be renamed as in PatCon98.

  • Revert the dynamic semantics section, use the one from the previous revision.

  • Talk briefly about the exhaustiveness check. I think simply saying that or patterns will be checked as if the alternative is expanded into two alternatives before checking would be enough.

This thread became impossible to follow so I'll include a changelog section in the proposal, talking briefly about what designs and we considered in this thread. This'll also help readers in the future to see what we considered but discarded.

@simonpj @goldfirere @nomeata could any of you share the .tex source of the paper so that I could extend Figure 4 with one more rule for or patterns?

@mchakravarty let's not merge this yet.

@gasche

This comment has been minimized.

Show comment
Hide comment
@gasche

gasche Jul 5, 2018

@nomeata I would think of the following rule

Γ ⊢ p₁ : σ => Γ, Δ₁
Γ ⊢ p₁ : σ => Γ, Δ₂
⌊Δ₁⌋ = Δ = ⌊Δ₂⌋
-------------------------
Γ ⊢ (p₁ ; p₂) : σ => Γ, Δ

where ⌊Δ⌋ removes all free variables α : ★ and constraints G from the context suffix Δ (but is undefined if that erasure was not well-formedness-preserving):

⌊ε⌋ = ε

⌊x:τ,Δ⌋ = x:τ, ⌊Δ⌋

⌊G,Δ⌋ = x:τ, ⌊Δ⌋

∀(x:τ)∈⌊Δ⌋, α∉τ
------------------
⌊α:★,Δ⌋ = x:τ, ⌊Δ⌋

The "full" rule for or-patterns, allowing capture of existentials and constraints, would not impose this erasure operation (so the condition is just Δ₁ = Δ = Δ₂). A judgment structure that separates the "old
context" from the "new context" (for example it could be only Γ ⊢ p : σ => Δ, where Δ is to be added to Γ in the CaseTv rule instead of just replacing it) would maybe make the algorithmic content of the rule clearer.

(Dynamic semantics: v matches (p1 ; p2) if it matches p1 or it matches p2.)

Note that I don't think writing such rules is necessary for the proposal -- for the purposes of being precise and helping people reason about the proposal, the desugaring specification is just fine.

gasche commented Jul 5, 2018

@nomeata I would think of the following rule

Γ ⊢ p₁ : σ => Γ, Δ₁
Γ ⊢ p₁ : σ => Γ, Δ₂
⌊Δ₁⌋ = Δ = ⌊Δ₂⌋
-------------------------
Γ ⊢ (p₁ ; p₂) : σ => Γ, Δ

where ⌊Δ⌋ removes all free variables α : ★ and constraints G from the context suffix Δ (but is undefined if that erasure was not well-formedness-preserving):

⌊ε⌋ = ε

⌊x:τ,Δ⌋ = x:τ, ⌊Δ⌋

⌊G,Δ⌋ = x:τ, ⌊Δ⌋

∀(x:τ)∈⌊Δ⌋, α∉τ
------------------
⌊α:★,Δ⌋ = x:τ, ⌊Δ⌋

The "full" rule for or-patterns, allowing capture of existentials and constraints, would not impose this erasure operation (so the condition is just Δ₁ = Δ = Δ₂). A judgment structure that separates the "old
context" from the "new context" (for example it could be only Γ ⊢ p : σ => Δ, where Δ is to be added to Γ in the CaseTv rule instead of just replacing it) would maybe make the algorithmic content of the rule clearer.

(Dynamic semantics: v matches (p1 ; p2) if it matches p1 or it matches p2.)

Note that I don't think writing such rules is necessary for the proposal -- for the purposes of being precise and helping people reason about the proposal, the desugaring specification is just fine.

osa1 added some commits Jul 5, 2018

Update leftover sentence about ambiguities
Since we updated the syntax to use `;` we have a different ambiguity:

`f C1 ; C2 C3 = ...` could mean:

- (C1 ; C2) C3
- C1 ; (C2 C3)
@simonpj

This comment has been minimized.

Show comment
Hide comment
@simonpj

simonpj Jul 5, 2018

Contributor

Because this desugaring rule can't be extended to handle GADTs

I don't agree. It handles GADTs perfectly well, describing precisely which GADT pattern matches are accepted and which are rejected. See the proposal!

I urge you not to remove the specification that the desugaring specifies both static and dynamic semantics. It is a very simple, complete, and comprehensible specification.

I'm all for, in addition, having an equivalent specification as typing judgements. That would be good, but it may not be straightforward. NB: if that specification turned out to disagree with the desugaring-based spec, I'd want us to discuss why, and which spec to adjust.

Specifying the static and dynamic semantics via desugaring does NOT rule out exhaustiveness checking. It's a specification not an implementation! We can do exhaustiveness checking by modifying the rules in "GADTS meet their match". There is no conflict here.

Contributor

simonpj commented Jul 5, 2018

Because this desugaring rule can't be extended to handle GADTs

I don't agree. It handles GADTs perfectly well, describing precisely which GADT pattern matches are accepted and which are rejected. See the proposal!

I urge you not to remove the specification that the desugaring specifies both static and dynamic semantics. It is a very simple, complete, and comprehensible specification.

I'm all for, in addition, having an equivalent specification as typing judgements. That would be good, but it may not be straightforward. NB: if that specification turned out to disagree with the desugaring-based spec, I'd want us to discuss why, and which spec to adjust.

Specifying the static and dynamic semantics via desugaring does NOT rule out exhaustiveness checking. It's a specification not an implementation! We can do exhaustiveness checking by modifying the rules in "GADTS meet their match". There is no conflict here.

@osa1

This comment has been minimized.

Show comment
Hide comment
@osa1

osa1 Jul 6, 2018

Contributor

I don't agree. It handles GADTs perfectly well, describing precisely which GADT pattern matches are accepted and which are rejected. See the proposal!

Sorry for my confusing wording. I meant it can't be extended for a better support for GADTs that involves binding constraints and dictionaries etc.

Contributor

osa1 commented Jul 6, 2018

I don't agree. It handles GADTs perfectly well, describing precisely which GADT pattern matches are accepted and which are rejected. See the proposal!

Sorry for my confusing wording. I meant it can't be extended for a better support for GADTs that involves binding constraints and dictionaries etc.

@simonpj

This comment has been minimized.

Show comment
Hide comment
@simonpj

simonpj Jul 6, 2018

Contributor

I meant it can't be extended for a better support for GADTs that involves binding constraints and dictionaries etc.

We have no such proposal on table, so it's hard to comment. I agree that at first sight there does not seem a natural extension. But I don't think that's a reason for abandoning it as our current specification. As I say, a more direct one using typing rules would be welcome too.,

Contributor

simonpj commented Jul 6, 2018

I meant it can't be extended for a better support for GADTs that involves binding constraints and dictionaries etc.

We have no such proposal on table, so it's hard to comment. I agree that at first sight there does not seem a natural extension. But I don't think that's a reason for abandoning it as our current specification. As I say, a more direct one using typing rules would be welcome too.,

@nomeata

This comment has been minimized.

Show comment
Hide comment
@nomeata

nomeata Jul 6, 2018

Contributor

Did someone experiment with a static desugaring rule that does not use View patterns, but rather join points? Here is an attempt (not for nested patterns yet, I did not think through how to that would work. But it better ought to, because really, that’s how we’d want to be able to implement or patterns efficiently!):

(I am using join point syntax here for clarification; of course since this is Haskell source, there is no difference to a normal let).

case scrut of (p1; p2) -> rhs
=
join j x1 … xn = rhs in
case scrut of { p1 -> jump j x1 … xn ; p2 -> jump j x1 … xn }

And now I wonder if this view (sic!) makes it easier to think about GADTs. What would happen if we specify

case scrut of (p1; p2) -> rhs
=
join j :: σ -- for some σ (which may involve constraints)
     j x1 … xn = rhs in
case scrut of { p1 -> jump j x1 … xn ; p2 -> jump j x1 … xn }

Wouldn’t that encompass all reasonable extensions of or-patterns to constriants and existentials? Although I can see that this specification could be considered too vague, because it now depends on the implementation to be “smart” in figuring out σ. Can we specify σ?

Actually, I wonder how much constraints and existential variables type inference would figure out with just the first desugaring (without a type signature)… Others here will have a better idea about that.

Contributor

nomeata commented Jul 6, 2018

Did someone experiment with a static desugaring rule that does not use View patterns, but rather join points? Here is an attempt (not for nested patterns yet, I did not think through how to that would work. But it better ought to, because really, that’s how we’d want to be able to implement or patterns efficiently!):

(I am using join point syntax here for clarification; of course since this is Haskell source, there is no difference to a normal let).

case scrut of (p1; p2) -> rhs
=
join j x1 … xn = rhs in
case scrut of { p1 -> jump j x1 … xn ; p2 -> jump j x1 … xn }

And now I wonder if this view (sic!) makes it easier to think about GADTs. What would happen if we specify

case scrut of (p1; p2) -> rhs
=
join j :: σ -- for some σ (which may involve constraints)
     j x1 … xn = rhs in
case scrut of { p1 -> jump j x1 … xn ; p2 -> jump j x1 … xn }

Wouldn’t that encompass all reasonable extensions of or-patterns to constriants and existentials? Although I can see that this specification could be considered too vague, because it now depends on the implementation to be “smart” in figuring out σ. Can we specify σ?

Actually, I wonder how much constraints and existential variables type inference would figure out with just the first desugaring (without a type signature)… Others here will have a better idea about that.

@gasche

This comment has been minimized.

Show comment
Hide comment
@gasche

gasche Jul 6, 2018

@nomeata in #43 (comment) I gave a compilation rule in the traditional style of pattern-matching compilation. It sort of generalizes your idea in a way that works well within sub-patterns as well -- in a more abstract notation that is familiar to the pattern-matching people.

gasche commented Jul 6, 2018

@nomeata in #43 (comment) I gave a compilation rule in the traditional style of pattern-matching compilation. It sort of generalizes your idea in a way that works well within sub-patterns as well -- in a more abstract notation that is familiar to the pattern-matching people.

@nomeata

This comment has been minimized.

Show comment
Hide comment
@nomeata

nomeata Jul 6, 2018

Contributor

Right, but that duplicates the RHSs, which we (at least those who have to deal with implementing it) certainly don’t want. A desguaring with a join point ensures that no RHS are duplicated, and that there is a way of mapping the possibly not identical things left of the => to a common denominator (which can hopefully now be more than “nothing”). Can you rephrase your compilation rule to something that uses a join point to that effect?

Contributor

nomeata commented Jul 6, 2018

Right, but that duplicates the RHSs, which we (at least those who have to deal with implementing it) certainly don’t want. A desguaring with a join point ensures that no RHS are duplicated, and that there is a way of mapping the possibly not identical things left of the => to a common denominator (which can hopefully now be more than “nothing”). Can you rephrase your compilation rule to something that uses a join point to that effect?

@gasche

This comment has been minimized.

Show comment
Hide comment
@gasche

gasche Jul 6, 2018

I believe that it is standard in pattern-matching compilers (but I'm not familiar enough with GHC's implementation to check this) to always by creating a label/join-point/exit for each rhs of the source pattern (parametrized over its captured variables), so that you can duplicate the rhs without issue. Indeed, typically other parts of pattern-matching compilation may also duplicate exits (typically patterns with _ will go both into known-constructor groups and the catch-all other-constructors group). This is orthogonal to or-patterns.

gasche commented Jul 6, 2018

I believe that it is standard in pattern-matching compilers (but I'm not familiar enough with GHC's implementation to check this) to always by creating a label/join-point/exit for each rhs of the source pattern (parametrized over its captured variables), so that you can duplicate the rhs without issue. Indeed, typically other parts of pattern-matching compilation may also duplicate exits (typically patterns with _ will go both into known-constructor groups and the catch-all other-constructors group). This is orthogonal to or-patterns.

@nomeata

This comment has been minimized.

Show comment
Hide comment
@nomeata

nomeata Jul 6, 2018

Contributor

But isn't the desire to not just say “duplicate and see what happens” the reason why we are unsure how well we can support GADTs? Because if we'd be happy with “duplicate and then type check” then we have great support for GADTs without any issues...

Contributor

nomeata commented Jul 6, 2018

But isn't the desire to not just say “duplicate and see what happens” the reason why we are unsure how well we can support GADTs? Because if we'd be happy with “duplicate and then type check” then we have great support for GADTs without any issues...

@gasche

This comment has been minimized.

Show comment
Hide comment
@gasche

gasche Jul 6, 2018

gasche commented Jul 6, 2018

@nomeata

This comment has been minimized.

Show comment
Hide comment
@nomeata

nomeata Jul 6, 2018

Contributor

The "full" rule for or-patterns, allowing capture of existentials and constraints, would not impose this erasure operation (so the condition is just Δ₁ = Δ = Δ₂).

I think this is too strong, I think. I would expect something like this:

Γ ⊢ p₁ : σ => Δ₁
Γ ⊢ p₁ : σ => Δ₂
Δ₁ ⊩ Δ
Δ₂ ⊩ Δ
-------------------------
Γ ⊢ (p₁ ; p₂) : σ => Δ

where the relation between contexts is a form of entailment or subset relation, with the idea that expressions that are well-typed in Δ are also well-typed in Δ₁ and Δ₂. Maybe this can be made more precise with a join operator on contexts that calculates the “common elements“ of two contexts, and a conclusion Γ ⊢ (p₁ ; p₂) : σ => Δ₁ ⊓ Δ₂.

Then, even this would type-check (as it, arguably, should)

maybeConst1 (Just x  ; Nothing) = 42
maybeConst2 (Just _x ; Nothing) = 42
maybeConst3 (Just _  ; Nothing) = 42

as the proposal currently stands, only the third is accepted, right? I am not sure I like that…

Here is another reason why “all alternatives bind exactly the same set of variables” will bite us. Maybe I want to write the following code, where I need to bind x in the left branch of the or, but not for the sake of bringing it into scope in the RHS, but in order to use in in a view pattern within the branch. But since I cannot easily prevent this from being bound there, the strict same-set-rule is causing issues here

foo :: (Maybe Int, Int) -> …
foo ((Just x, ((> x) -> True)); (Nothing, _)) = …
Contributor

nomeata commented Jul 6, 2018

The "full" rule for or-patterns, allowing capture of existentials and constraints, would not impose this erasure operation (so the condition is just Δ₁ = Δ = Δ₂).

I think this is too strong, I think. I would expect something like this:

Γ ⊢ p₁ : σ => Δ₁
Γ ⊢ p₁ : σ => Δ₂
Δ₁ ⊩ Δ
Δ₂ ⊩ Δ
-------------------------
Γ ⊢ (p₁ ; p₂) : σ => Δ

where the relation between contexts is a form of entailment or subset relation, with the idea that expressions that are well-typed in Δ are also well-typed in Δ₁ and Δ₂. Maybe this can be made more precise with a join operator on contexts that calculates the “common elements“ of two contexts, and a conclusion Γ ⊢ (p₁ ; p₂) : σ => Δ₁ ⊓ Δ₂.

Then, even this would type-check (as it, arguably, should)

maybeConst1 (Just x  ; Nothing) = 42
maybeConst2 (Just _x ; Nothing) = 42
maybeConst3 (Just _  ; Nothing) = 42

as the proposal currently stands, only the third is accepted, right? I am not sure I like that…

Here is another reason why “all alternatives bind exactly the same set of variables” will bite us. Maybe I want to write the following code, where I need to bind x in the left branch of the or, but not for the sake of bringing it into scope in the RHS, but in order to use in in a view pattern within the branch. But since I cannot easily prevent this from being bound there, the strict same-set-rule is causing issues here

foo :: (Maybe Int, Int) -> …
foo ((Just x, ((> x) -> True)); (Nothing, _)) = …
@goldfirere

This comment has been minimized.

Show comment
Hide comment
@goldfirere

goldfirere Jul 6, 2018

Contributor

I like that rule with "join". Implementing it seems challenging. We would probably have to make type-checking patterns return an explicit partial type env't (partial in the sense that it's not an extension of the existing type env't) and then we could do the join operation. (Of course, we could do the join on the complete env't, but that would seem slow.)

Contributor

goldfirere commented Jul 6, 2018

I like that rule with "join". Implementing it seems challenging. We would probably have to make type-checking patterns return an explicit partial type env't (partial in the sense that it's not an extension of the existing type env't) and then we could do the join operation. (Of course, we could do the join on the complete env't, but that would seem slow.)

@gasche

This comment has been minimized.

Show comment
Hide comment
@gasche

gasche Jul 6, 2018

@nomeata From a soundness perspective, of course you are right. It is also
possible to add a separate weakening rules, letting a pattern drop
a variable from its captured environment, and recover your rule as
a composition of both.

From a usability perspective, you have to be careful. If the user
writes (Foo a_b; Bar ab), it is likely that they did not intend to
capture nothing but made a typo. Delaying this realization until the
user tries to use a variable is not very nice.
Languages have their usability conventions on which bindings are
allowed to remain unused, and which rather should not. (Typically you
can not-use the _foo, but not the other term variables.) It sounds
natural to follow those when deciding which term variables may be
dropped in patterns, in particular on either sides of an or-pattern.

Existential type variables and constraints are not explicitly named by
the user, so there is no intent to guide here, and it is reasonable to
allow dropping them by default. This is, in particular, what the
"simplest GADT proposal" does by forcing that they are all dropped on
both sides.

gasche commented Jul 6, 2018

@nomeata From a soundness perspective, of course you are right. It is also
possible to add a separate weakening rules, letting a pattern drop
a variable from its captured environment, and recover your rule as
a composition of both.

From a usability perspective, you have to be careful. If the user
writes (Foo a_b; Bar ab), it is likely that they did not intend to
capture nothing but made a typo. Delaying this realization until the
user tries to use a variable is not very nice.
Languages have their usability conventions on which bindings are
allowed to remain unused, and which rather should not. (Typically you
can not-use the _foo, but not the other term variables.) It sounds
natural to follow those when deciding which term variables may be
dropped in patterns, in particular on either sides of an or-pattern.

Existential type variables and constraints are not explicitly named by
the user, so there is no intent to guide here, and it is reasonable to
allow dropping them by default. This is, in particular, what the
"simplest GADT proposal" does by forcing that they are all dropped on
both sides.

@nomeata

This comment has been minimized.

Show comment
Hide comment
@nomeata

nomeata Jul 6, 2018

Contributor

@simonpj @goldfirere @nomeata could any of you share the .tex source of the paper so that I could extend Figure 4 with one more rule for or patterns?

I put the ott source for typesetting the typing rules of the paper in this gist:
https://gist.github.com/nomeata/e7725c795d36c9ac7fbc45461a7266dd

Contributor

nomeata commented Jul 6, 2018

@simonpj @goldfirere @nomeata could any of you share the .tex source of the paper so that I could extend Figure 4 with one more rule for or patterns?

I put the ott source for typesetting the typing rules of the paper in this gist:
https://gist.github.com/nomeata/e7725c795d36c9ac7fbc45461a7266dd

@nomeata

This comment has been minimized.

Show comment
Hide comment
@nomeata

nomeata Jul 6, 2018

Contributor

Delaying this realization until the user tries to use a variable is not very nice.

Why? An error message “a_b is out of scope here. It is bound in one branch of the or-pattern at position x:y, but not the others” will quickly point the user to the problem – I don’t see a problem here.

Contributor

nomeata commented Jul 6, 2018

Delaying this realization until the user tries to use a variable is not very nice.

Why? An error message “a_b is out of scope here. It is bound in one branch of the or-pattern at position x:y, but not the others” will quickly point the user to the problem – I don’t see a problem here.

@nomeata

This comment has been minimized.

Show comment
Hide comment
@nomeata

nomeata Jul 6, 2018

Contributor

The section “Interaction with other extensions” should discuss the interaction with ScopedTypeVariables, and with type variables bound in patterns. For example, would these be accepted?

foo :: Either a a -> …
foo (Left (x :: b) ; Right (x :: b)) = …

bar :: Either a a -> …
bar (Left (x :: b) ; Right x) = … -- with no mention of b on the RHS

It may be that a desugaring with view patterns will not be able to answer or express these questions, as we cannot return a type variable in the Just of that desugaring. (Unless the rule is “no type variables bound in or-patterns, never”. Which would be sad and probably be revised at some point.)

Contributor

nomeata commented Jul 6, 2018

The section “Interaction with other extensions” should discuss the interaction with ScopedTypeVariables, and with type variables bound in patterns. For example, would these be accepted?

foo :: Either a a -> …
foo (Left (x :: b) ; Right (x :: b)) = …

bar :: Either a a -> …
bar (Left (x :: b) ; Right x) = … -- with no mention of b on the RHS

It may be that a desugaring with view patterns will not be able to answer or express these questions, as we cannot return a type variable in the Just of that desugaring. (Unless the rule is “no type variables bound in or-patterns, never”. Which would be sad and probably be revised at some point.)

@nomeata

This comment has been minimized.

Show comment
Hide comment
@nomeata

nomeata Jul 7, 2018

Contributor

Another language extension interaction worth discussing: RecordWildCards. Consider this:

data T
  = C1 { a :: Int }
  | C2 { a :: Int }
  | C3 { a :: Int, b :: Bool}
  | C4 { a :: Int, b :: Char}

foo1 (C1 {...}; C2 {...}) = a
foo2 (C1 {...}; C3 {...}) = a
foo3 (C3 {...}; C4 {...}) = a
foo4 (C3 {...}; C4 {...}) = if b then a else 0

Currently, the proposal wording accepts foo1 but rejects all the others.

I am currently inclined to think that this is unnecessary strict, and am wondering if it would not be nicer if it accepts foo1, foo2 and foo3, and rejects foo4. A possible error message might be

You cannot use b here, as its type differs in different branches of the or-pattern in …:
b is bound with type Bool in …, but
b is bound with type Char in …

Of course the proposal ought not specify the wording of error messages, this is just for illustration.

This is related to my earlier point about

maybeConst1 (Just x  ; Nothing) = 42
maybeConst2 (Just _x ; Nothing) = 42
maybeConst3 (Just _  ; Nothing) = 42

One might argue that the first line is bad style (and I agree). But note that even if the proposal allows the former, then the usual “unused bindings warning mechanism” will ensure that -Wall-warning-free-code will not contain the first form,while allowing the second and the third, just as desired.

Contributor

nomeata commented Jul 7, 2018

Another language extension interaction worth discussing: RecordWildCards. Consider this:

data T
  = C1 { a :: Int }
  | C2 { a :: Int }
  | C3 { a :: Int, b :: Bool}
  | C4 { a :: Int, b :: Char}

foo1 (C1 {...}; C2 {...}) = a
foo2 (C1 {...}; C3 {...}) = a
foo3 (C3 {...}; C4 {...}) = a
foo4 (C3 {...}; C4 {...}) = if b then a else 0

Currently, the proposal wording accepts foo1 but rejects all the others.

I am currently inclined to think that this is unnecessary strict, and am wondering if it would not be nicer if it accepts foo1, foo2 and foo3, and rejects foo4. A possible error message might be

You cannot use b here, as its type differs in different branches of the or-pattern in …:
b is bound with type Bool in …, but
b is bound with type Char in …

Of course the proposal ought not specify the wording of error messages, this is just for illustration.

This is related to my earlier point about

maybeConst1 (Just x  ; Nothing) = 42
maybeConst2 (Just _x ; Nothing) = 42
maybeConst3 (Just _  ; Nothing) = 42

One might argue that the first line is bad style (and I agree). But note that even if the proposal allows the former, then the usual “unused bindings warning mechanism” will ensure that -Wall-warning-free-code will not contain the first form,while allowing the second and the third, just as desired.

@nomeata

This comment has been minimized.

Show comment
Hide comment
@nomeata

nomeata Jul 8, 2018

Contributor

I had a long nice walk with Richard and we talked about possible ambitious ways of approaching or patterns (what if types are not equal, but subtypes? what about existential variables? what about constraints). We had many intriguing ideas, but nothing that immediately clicked or solved all the problems…

I hope that one day we figure out how to make or-pattern so strong that for all patterns p, p;p is equivalent to p (even if it involves GADts etc.), but I don’t require it for the first iteration of this feature, and will not veto a variant that only brings term variables into scope, and only if their types match.

I would still like to discuss the (not very technical, and more bikesheddingly) question of whether it is a compile error if some unused variables don’t meet this requirement.

Contributor

nomeata commented Jul 8, 2018

I had a long nice walk with Richard and we talked about possible ambitious ways of approaching or patterns (what if types are not equal, but subtypes? what about existential variables? what about constraints). We had many intriguing ideas, but nothing that immediately clicked or solved all the problems…

I hope that one day we figure out how to make or-pattern so strong that for all patterns p, p;p is equivalent to p (even if it involves GADts etc.), but I don’t require it for the first iteration of this feature, and will not veto a variant that only brings term variables into scope, and only if their types match.

I would still like to discuss the (not very technical, and more bikesheddingly) question of whether it is a compile error if some unused variables don’t meet this requirement.

@simonpj

This comment has been minimized.

Show comment
Hide comment
@simonpj

simonpj Jul 9, 2018

Contributor

It's interesting how much is hidden inside such an apparently small innovation as or-patterns!

I'm still liking the spec "if the desugaring to view patterns typechecks, so does the or-pattern", because it is so simple and explicable. I'd love to see a direct typing rule for this; I don't think it should be too hard.

Unused variables. A variant of the design, which Joachim is implicitly suggesting, is to say that (p1 ; p2) brings into scope all the variables that are bound by both p1 and p2. That is, the two sets do not have to be identical; but only variables bound by both are brought into scope by the or-pattern.

I quite like that idea:

  • It is still readily explicable by the desugaring rule. It really does not make the spec more complicated.

  • It allows a variable to be bound in p1 and used in a view pattern in p1 without messing up the overall or-pattern (Joachim's point above).

  • It would allow dot-dot notation (which implicitly binds all the variables of the record).

So that change seems like a modest win to me. The only downside is that given

f (Just x ; Nothing) = x

you might just get "x is not in scope" (as indeed it isn't) and be puzzled. But of course extra work on the renamer could produce a more informative error message.

Scoped type variables

I can see that the specify-via-view-patterns story does not allow any scoped type variables to be brought into scope, even if they don't involve existentials. That's a shortcoming.

To me that's the strongest argument for a more direct typing rule. But unless someone wants to do that work soon, and it happens to work out really smoothly, I don't think we should let it stand in the way of doing something simpler for now.

Contributor

simonpj commented Jul 9, 2018

It's interesting how much is hidden inside such an apparently small innovation as or-patterns!

I'm still liking the spec "if the desugaring to view patterns typechecks, so does the or-pattern", because it is so simple and explicable. I'd love to see a direct typing rule for this; I don't think it should be too hard.

Unused variables. A variant of the design, which Joachim is implicitly suggesting, is to say that (p1 ; p2) brings into scope all the variables that are bound by both p1 and p2. That is, the two sets do not have to be identical; but only variables bound by both are brought into scope by the or-pattern.

I quite like that idea:

  • It is still readily explicable by the desugaring rule. It really does not make the spec more complicated.

  • It allows a variable to be bound in p1 and used in a view pattern in p1 without messing up the overall or-pattern (Joachim's point above).

  • It would allow dot-dot notation (which implicitly binds all the variables of the record).

So that change seems like a modest win to me. The only downside is that given

f (Just x ; Nothing) = x

you might just get "x is not in scope" (as indeed it isn't) and be puzzled. But of course extra work on the renamer could produce a more informative error message.

Scoped type variables

I can see that the specify-via-view-patterns story does not allow any scoped type variables to be brought into scope, even if they don't involve existentials. That's a shortcoming.

To me that's the strongest argument for a more direct typing rule. But unless someone wants to do that work soon, and it happens to work out really smoothly, I don't think we should let it stand in the way of doing something simpler for now.

@Centril

This comment has been minimized.

Show comment
Hide comment
@Centril

Centril Aug 31, 2018

For reference, I have written the moral equivalent of this proposal for Rust, rust-lang/rfcs#2535.

Centril commented Aug 31, 2018

For reference, I have written the moral equivalent of this proposal for Rust, rust-lang/rfcs#2535.

@klapaucius

This comment has been minimized.

Show comment
Hide comment
@klapaucius

klapaucius Sep 12, 2018

@mchakravarty

https://mail.haskell.org/pipermail/ghc-steering-committee/2018-June/000646.html

Fri Jun 29 03:16:30 UTC 2018
I will mark the proposal as accepted unless I hear a dissenting opinion by the end of next week.

klapaucius commented Sep 12, 2018

@mchakravarty

https://mail.haskell.org/pipermail/ghc-steering-committee/2018-June/000646.html

Fri Jun 29 03:16:30 UTC 2018
I will mark the proposal as accepted unless I hear a dissenting opinion by the end of next week.

@osa1

This comment has been minimized.

Show comment
Hide comment
@osa1

osa1 Sep 12, 2018

Contributor

As said above this is not ready for merging yet. There are still a lot of things to specify.

Contributor

osa1 commented Sep 12, 2018

As said above this is not ready for merging yet. There are still a lot of things to specify.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment