Skip to content
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

Invisible binders in type declarations #425

Merged
merged 5 commits into from Apr 7, 2022

Conversation

int-index
Copy link
Contributor

@int-index int-index commented Jun 7, 2021

The proposal has been accepted; the following discussion is mostly of historic interest.


This is my rewrite of #326 + #386.

We propose to allow invisible type variable binders (i.e. @k) in type declarations. Here are a few examples:

In a class declaration:

class C @k (a :: k) where ...
--     ^^^^

In a data type declaration:

data D @k @j (a :: k) (b :: j) = ...
--    ^^^^^^^

In a type family declaration:

type family F @p @q (a :: p) (b :: q) where ...
--           ^^^^^^^

We then propose to use these binders to simplify scoping rules and arity inference.

Rendered

@int-index int-index marked this pull request as draft June 7, 2021 21:31
@Ericson2314
Copy link
Contributor

Looking good so far! Per #386 (comment) I think you want the scoping and instantiation rules for data families too?

@jvanbruegge
Copy link

Does this proposal touch in any way the way implicit pattern matching in type families work? I mean if I pattern match on types with different kinds, GHC implicitly pattern matches on their kinds first which can lead to surprising behavior (partiality when one does not expect it naively). With this proposal, it is possible to bind invisible arguments and explicitly pattern match on them, so would it be possible to disable that implicit pattern match completely and present an error to the user?

@int-index
Copy link
Contributor Author

@jvanbruegge The proposal disallows matching on implicit kind parameters on the RHS, so the following will become illegal:

type family F a :: k

type instance F Int = Char
type instance F Int = Maybe

You would need to rewrite it as:

type instance F @Type         Int = Char
type instance F @(Type->Type) Int = Maybe

However, if matching happens on the LHS, it is still fair game:

type family G (a :: k) :: Natural

type instance G Char = 0
type instance G Maybe = 1
type instance G True = 2

For clarity, we could rewrite it as:

type instance G @Type Char = 0
type instance G @(Type -> Type) Maybe = 1
type instance G @Bool True = 2

But in this case it is not required.

@jvanbruegge
Copy link

jvanbruegge commented Jun 8, 2021

Ok, cool. Yeah, I meant the first case

Copy link
Contributor

@goldfirere goldfirere left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like where this is going. Many thanks for writing this up!

proposals/0000-decl-invis-binders.rst Show resolved Hide resolved
proposals/0000-decl-invis-binders.rst Outdated Show resolved Hide resolved
proposals/0000-decl-invis-binders.rst Outdated Show resolved Hide resolved
proposals/0000-decl-invis-binders.rst Show resolved Hide resolved
proposals/0000-decl-invis-binders.rst Show resolved Hide resolved
proposals/0000-decl-invis-binders.rst Outdated Show resolved Hide resolved
proposals/0000-decl-invis-binders.rst Outdated Show resolved Hide resolved
type family G x :: forall k. Maybe k

With ``@``-binders we can do the opposite. We propose that by default, arity
inference would include as few forall-bound variables as possible, to allow
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More than that, the arity would no longer be strangely influenced by the return kind signature. Instead, you can simply count the binders to the left the :: (if any).

Sigh: not quite. In

F :: forall k. k -> Type
type family F a

F has arity 2, not 1.

proposals/0000-decl-invis-binders.rst Show resolved Hide resolved
@simonpj
Copy link
Contributor

simonpj commented Jun 9, 2021

I have added some comments

I am happy to see that you have included

  • In type family and data family instances, the instantiation is fully determined by the left hand side, without looking at the right hand side.

but you have zero text motivating, or even specifying, this point. There is plenty to plunder in https://github.com/ghc-proposals/ghc-proposals/blob/wip/spj-instance-scoping/proposals/0000-instance-scoping.rst

@int-index int-index marked this pull request as ready for review July 13, 2021 11:16
@int-index
Copy link
Contributor Author

This is ready for another round of review.

@nomeata
Copy link
Contributor

nomeata commented Jul 13, 2021

The title still says “Draft”. Oversight?

@int-index int-index changed the title Draft: Invisible binders in type declarations Invisible binders in type declarations Jul 13, 2021
Copy link
Contributor

@goldfirere goldfirere left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm quite happy with this! Thanks.

5. In type synonym declarations, require that every variable mentioned on the
RHS must be bound on the LHS.

6. In type family and data family instances, require that every variable
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part is completely separable from the rest of the proposal, I think. Is this stated somewhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It’s implied by putting it into a section titled “Secondary Change”.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but I would expect a "secondary change" to depend on the primary one. This one has no relationship, really, to the rest of the proposal. I'm all for it -- just got surprised at first at seeing a change to instance syntax in a proposal about type syntax.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, this proposal bundles a few unrelated changes together.

Secondary Change: Instantiation Rules
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

7. In type family and data family instances, the instantiation is fully
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This, similarly, is completely separable form the rest of the proposal -- even the other point about instances. They are in sympathy with each other (and, indeed, the rest of the proposal), but there is no dependency between the rest of this proposal and these two points about instances. Right?

* Changes (1), (2), and (3) provide the programmer with a more principled way
of brining type variables into scope in certain corner cases.

* Changes (4), (5), and (6) simplify arity inference and scoping rules, but they are
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(6) does not need a migration strategy, because the fix is backward-compatible.

I'm not very bothered about (4), which is rather exotic.

For (5), I would say to issue a warning for a few releases before pulling the trigger. I don't think that would be hard to implement.

@int-index
Copy link
Contributor Author

@nomeata I'd like to submit this proposal to the committee.

@nomeata nomeata added the Pending shepherd recommendation The shepherd needs to evaluate the proposal and make a recommendataion label Aug 13, 2021
@nomeata nomeata changed the title Invisible binders in type declarations Invisible binders in type declarations (under review) Aug 13, 2021
@int-index
Copy link
Contributor Author

@goldfirere In case this slipped your attention, do you plan to make a recommendation?

@simonpj
Copy link
Contributor

simonpj commented Oct 19, 2021

@goldfirere Richard, shall we move forward on this one?

@goldfirere
Copy link
Contributor

Somehow, this fell off my radar -- apologies. I have recommended acceptance, but with a migration strategy around point (5).

@simonpj
Copy link
Contributor

simonpj commented Oct 22, 2021

When a standalone kind signature is given, modify the process of zipping binders with quantifiers

Vlad, this is pretty impenetrable. zipSAKs is not something most programmers are going to know about -- indeed I do not. Could you reword this bullet to be addressed to a programmer? And give examples. (Either inline here, or forward referenced ("see Example 4,5,6 below").

Earlier you say "SAKS zipping works over two lists: quantifiers and binders. Let us define it in pseudo-code:". At least give a type signature to this function. But more importantly: so what? Now you have defined zipSAKS. Why have you told me this?

During arity inference, do not include trailing forall x. quantifiers in the arity.

This is a rather negative explanation. Why not say what we do? Something like:

The arity of a type-family declaration

type family F b1 .. bn

is determined solely by the visible arguments b1 ... bn in the type family declaration. An application of F is saturated iff it is applied to all these n arguments. The kind signature (if any) for F does not affect its arity. For example:

type F1 ::: Type -> Type -> Type
type F1 a           -- Has arity 1 not 2

type F2 :: Type -> forall k. k -> Type
type F2 a            -- Still has arity 1

type F3 a :: forall k. k -> Type     -- Still arity 1

type F4 @k (a :: k)                                    -- Arity 2
type F5 (a :: Type) @k  :: k -> Type           -- Arity 2

You may want to also point out that there may be invisible inferred kind args.

@simonpj
Copy link
Contributor

simonpj commented Oct 22, 2021

I strongly support this proposal, because it lets us say explicitly things that we can only currently say implicitly. It fills out part of the design envelope. Let's do it.

@goldfirere
Copy link
Contributor

Vlad, this is pretty impenetrable.

In sympathy with what I just posted at #442 (comment), I'm fine with the current version of this proposal -- at least until we accept. Perhaps if this is to be long-lasting, it's worth improving the presentation. But, even there, I'm not convinced: the description of this feature belongs in the user manual, not in this repo. And the proposal is burdened with capturing the change from the status quo, which need not appear in the manual. I agree that the current presentation is hard-going, and it has room for various improvements. But I'm equally confident I understand the intent of this proposal and could evaluate whether an implementation meets the specification presented here, and so I don't see the value in the time investment of improving the presentation.

To be clear, I would consider a presentation like the one in this proposal below the bar ("unacceptable" sounds very harsh, but is semantically accurate) for the user manual.

@int-index
Copy link
Contributor Author

int-index commented Oct 25, 2021

What we explain to our users (in the User’s Guide) does not need to involve zipSAKS, but it also doesn’t need to be precise: as long as the programmer has a rough understanding of what’s going on, they can start using the feature, guided by the compiler diagnostics.

But in the “Proposed Change Specification” my goal wasn’t to explain the feature to the end user. Rather, I wanted to specify it in a way that would be precise enough to guide the implementation efforts and to cover all corner cases.

Honestly, I have no idea how to do it without introducing the notion of SAKS zipping.

Now you have defined zipSAKS. Why have you told me this?

It’s the function that determines which binder corresponds to which quantifier. So if you have

type T :: forall a b. a -> forall c. blah
type T @v t @w = ...

What do v and w correspond to? I say v is a, and w is c, but how come we skipped b? This is determined by zipSAKS.

@int-index
Copy link
Contributor Author

is determined solely by the visible arguments b1 ... bn in the type family declaration.

But that’s simply not the case. Consider:

type family F a

What arity does it have? Depends on the kind signature:

type F :: Type -> Type          -- arity 1
type F :: forall k. k -> Type   -- arity 2

@simonpj
Copy link
Contributor

simonpj commented Oct 26, 2021

What arity does it have? Depends on the kind signature:

Hmm, yes you are right. It's quite hard to specify arity. And yet we must.

Perhaps if this is to be long-lasting, it's worth improving the presentation. But, even there, I'm not convinced: the description of this feature belongs in the user manual, not in this repo.

As I say, I'm strongly in favour of accepting this proposal, and that opinion is not conditional on improved presentation.

But I know I'm going to be asked to do code review. What often happens is that I'm trying to review code against a specification that is absent or at least hard to understand. I'm trying to avoid that. So I'm reading the proposal thinking "could I implement this?", and writing comments accordingly.

I'm perfectly content with a specification presented as a user-manual entry. But (unlike @goldfirere) I would say that a draft user manual entry would be a terrific component of a proposal.

It can be written post-acceptance -- I'm not trying to raise the bar for a proposal that might be rejected. My anxiety is that post-acceptance the author breathes a well-deserved sigh of relief, and the improved presentation, the more detailed specification, or the user manual entry, ultimately never gets written.

I strongly agree it's much better not presented as a diff against some (also often ill-specified) baseline.

@goldfirere
Copy link
Contributor

What evaluation process exactly do you have in mind?

A post-proposal-acceptance process. Or, if you prefer, I'd argue for conditional acceptance here, depending on what we learn during implementation. (Truth be told, I consider all proposals conditionally accepted on similar grounds. It is always possible to learn some new devastating fact during implementation.) Maybe this plan of mine makes it all more concrete:

  1. Blast to Discourse (and maybe a few other places) seeking feedback on the breakage.
  2. Discuss.
  3. If the discussion suggests that doing so is sensible, advocate to the committee for acceptance.
  4. Accept the proposal.
  5. Implement (this is your part).
  6. Assess breakage in practice on head.hackage, along at least two different axes: how many different places break, and how hard each breakage is to diagnose & fix. That is, even if each fix is trivial, if every package breaks, that's bad. Even if there are very few breakages, if the breakage requires the whole package to be rewritten, that's bad. (To be clear, I expect neither of these.)
  7. Report on this breakage back out to the community, and discuss briefly.
  8. Depending on the result of that discussion, merge.

We're operating here expecting breakage both to be relatively rare and easy to diagnose & fix. If these assumptions are correct, then these last steps will go swimmingly. If we're wrong, then it will be good to re-examine things.

My comment "Do you think that's fair?" is about setting expectations: I wouldn't want to have this plan in mind only to have you and your colleagues very upset when I try to take Step 7.

@Ericson2314
Copy link
Contributor

Ericson2314 commented Mar 14, 2022

I would say we should be always able to merge the feature, but it might need a deprecation cycle. For example, simplified subsumption could have gotten a deprecation cycle rather than being cancelled outright as many people probably wished after being unable to upgrade to 9.0.

I too need to write a discorse post, but about how the sort of modularity work @hsyl20 and I are into directly relates to our ability to do proper depreciation cycles without drowning in hard-to-test conditional code.

@int-index
Copy link
Contributor Author

Maybe this plan of mine makes it all more concrete

@goldfirere That’s a fine plan, and thanks for taking the initiative here.

@goldfirere
Copy link
Contributor

I have created posts on Discourse, Reddit, and Twitter. Will review in a few days.

/remind me to review responses on Thursday.

@reminders-prs
Copy link

reminders-prs bot commented Mar 21, 2022

@goldfirere set a reminder for Mar 24th 2022

@AntC2
Copy link
Contributor

AntC2 commented Mar 22, 2022

@goldfirere I have created posts on ...

Hehe Reddit thought it was spam. (What on earth triggered their algorithm?)

@tomjaguarpaw
Copy link
Contributor

Reddit's zealous spam filter snares me about 10% of the time I post a story. I messaged the mods to request they restore it.

@int-index int-index removed the Needs revision The proposal needs changes in response to shepherd or committee review feedback label Mar 23, 2022
@reminders-prs reminders-prs bot removed the reminder label Mar 24, 2022
@reminders-prs
Copy link

reminders-prs bot commented Mar 24, 2022

👋 @goldfirere, review responses .

@goldfirere
Copy link
Contributor

All seems in order here. In particular, the community outreach did not produce any red flags. I am (re-)recommending acceptance to the committee. Thanks!

@goldfirere
Copy link
Contributor

/remind me to accept this proposal (pending committee agreement) on April 4.

@reminders-prs
Copy link

reminders-prs bot commented Mar 24, 2022

@goldfirere set a reminder for Apr 4th 2022

@JakobBruenker
Copy link
Contributor

JakobBruenker commented Mar 24, 2022

the community outreach did not produce any red flags

@goldfirere Note that the reddit post is still marked as [removed]
(Which I imagine means the moderators haven't read @tomjaguarpaw's message yet)

@goldfirere
Copy link
Contributor

@JakobBruenker That's true (and I was aware of it). But given the quietness both on Discord and Twitter, I decided not to investigate. If you want to get Reddit unblocked, great! There's no reason that we can't revisit the committee discussion in light of new information from Reddit. But I'm afraid I'm not going to take another crack at Reddit on this front.

@goldfirere
Copy link
Contributor

@phadej points out (on Reddit, but the thread is mostly about something else) that this proposal violates the Explicit Variables Proposal, included in #448, because there is no way to bring inferred variables into scope without unification. (Actually, the same is true within #448.) This suggests that this proposal might benefit from the @(..) syntax proposed with #448.

I do not suggest we change this proposal at this point, just that there is an improvement possible in a later proposal.

@Ericson2314
Copy link
Contributor

That sounds good. If #448 pulls some of this under it's umbrella, that's fine, but there is enough separate stuff on arity not befitting #448 that I agree that's not a good reason to delay this any further.

@reminders-prs
Copy link

reminders-prs bot commented Apr 4, 2022

👋 @goldfirere, accept this proposal (pending committee agreement) on .

@nomeata nomeata added the Pending committee review The committee needs to evaluate the proposal and make a decision label Apr 7, 2022
@nomeata nomeata merged commit a6c818a into ghc-proposals:master Apr 7, 2022
@nomeata nomeata changed the title Invisible binders in type declarations (under review) Invisible binders in type declarations Apr 7, 2022
@nomeata nomeata added Accepted The committee has decided to accept the proposal and removed Pending committee review The committee needs to evaluate the proposal and make a decision labels Apr 7, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Accepted The committee has decided to accept the proposal
Development

Successfully merging this pull request may close these issues.

None yet

9 participants