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

SIP-47 - Clause Interleaving #47

Merged
merged 8 commits into from
Oct 21, 2022
Merged

Conversation

Sporarum
Copy link
Contributor

@Sporarum Sporarum commented Aug 17, 2022

Edit: Other PRs have since modified this document, for the latest version, go here

We propose to generalize method signatures to allow any number of type parameter lists, interleaved with term parameter lists and using parameter lists. As a simple example, it would allow to define

def pair[A](a: A)[B](b: B): (A, B) = (a, b)

Here is also a more complicated and contrived example that highlights all the possible interactions:

def foo[A][B <: A](using a: A)(b: B)[C <: a.type, D](cd: (C, D))[E]: Foo[A, B, C, D, E]

(taken from the summary section)

@sjrd sjrd changed the title Clause Interleaving SIP-47 Clause Interleaving Aug 17, 2022
@sjrd sjrd changed the title SIP-47 Clause Interleaving SIP-47 - Clause Interleaving Aug 17, 2022
@kitbellew
Copy link

kitbellew commented Aug 29, 2022

We propose to generalize method signatures to allow any number of type parameter lists, interleaved with term parameter lists and using parameter lists. As a simple example, it would allow to define

def pair[A](a: A)[B](b: B): (A, B) = (a, b)

Here is also a more complicated and contrived example that highlights all the possible interactions:

def foo[A][B <: A](using a: A)(b: B)[C <: a.type, D](cd: (C, D))[E]: Foo[A, B, C, D, E]

(taken from the summary section)

what about

def pair[A1, A2](a1: A1, a2: A2)[B1, B2, B3](b1: B1, b2: B2, b3: B3): (A1, B3) = (a1, b3)

or

def pair[A1, A2][B1, B2, B3](a1: A1, a2: A2)(b1: B1, b2: B2, b3: B3): (A1, B3) = (a1, b3)

@Sporarum
Copy link
Contributor Author

Yes of course !
If you haven't already, do read the "changed file" (even though it's really an added file), it should give you a full picture of what this feature allows

And if anything's not clear, feel free to ask
(if possible as a PR comment, so I can reply more easily)

@Kordyjan
Copy link
Contributor

Kordyjan commented Sep 1, 2022

While I agree that what is proposed in this SIP would be a valuable addition to Scala. I remember myself having to write intermediate classes with apply methods to circumvent the single type param clause limitation.

However, right now in Scala 3 there are some ways to achieve that. For example, if I'm not mistaken:

def getOrElse(key: Key)[V >: key.Value](default: V): V = ???

from the proposal, can be right now defined as

def getOrElse: (k:Key) => [V >: k.Value] => (default: V) => V = 
    (k: Key) => [V] => (default: V) => ???

Of course, the current approach is much less visually pleasing and has some (quite arbitrary) limitations. For example, it is not legal to declare

def makePair: [A] => [B] => (A, B) = ???

while I assume your proposal would allow

def makePair[A][B]: (A, B) = ???

I think the proposal itself should address that. It should make a statement about why we should add a new thing to the language that will have only very niche use cases, instead of improving already existing mechanisms.

@Sporarum
Copy link
Contributor Author

Sporarum commented Sep 2, 2022

@Kordyjan, thank you for your feedback !
I added a section on that, let me know what you think

@Sporarum
Copy link
Contributor Author

Sporarum commented Sep 8, 2022

@Kordyjan I added a section addressing what you asked, what do you think ?

@Kordyjan
Copy link
Contributor

Kordyjan commented Sep 8, 2022

Sorry for not answering earlier. The section indeed addresses what I thought was missing in the proposal.

About the proposal itself: I think it is well thought-out. It feels much more like lifting an arbitrary limitation than adding a new feature to the language. As I said, it would have made much code that I wrote in the past clearer and easier to follow. My only fear was that it would make it possible to write unnecessarily complicated signatures for a small benefit, making libraries relying heavily on this feature scary for the newcomers. However, I haven't found anything worse than what the current language features allow library authors to create.

Therefore I'm provisionally voting to accept this proposal. However, I'm curious about the opinions of other reviewers.

@Sporarum
Copy link
Contributor Author

Sporarum commented Sep 9, 2022

@soronpo, @chrisandrews-ms, I have not heard from you, what do you think of the proposal ?

@soronpo
Copy link
Contributor

soronpo commented Sep 9, 2022

Apologies, I remembered I was not assigned to it at the beginning, and didn't notice I was assigned after.
For motivation, you should mention that it resolves an extension method feature limitation (need to find the relevant issue).
I'm in favor of accepting this proposal, but I should mention that to accept it after the implementation stage is a much greater challenge. The feature interaction and possible combinations here are massive! To be able to properly test all that and report good error messages may be very difficult.

@Sporarum
Copy link
Contributor Author

Sporarum commented Sep 9, 2022

extension method feature limitation

Do you mean extension [T](...) def foo[U](...) ?
If so this has already been added, and at the time, the codebase was refactored to deal with arbitrarily many type clauses, so your point:

accept it after the implementation stage is a much greater challenge

Might not actually be that challenging, as the implementation boils down to "modify the parser", since the generated tree is already valid in the rest of the compiler

@Sporarum
Copy link
Contributor Author

Sporarum commented Sep 9, 2022

I will add two things to the proposal later today:
The fact that

def getOrElse(k:Key): [V >: k.Value] => (default: V) => V = 
    (k: Key) => [V] => (default: V) => ???

Poses problem with overloading resolution, if there is another def getOrElse(k:Key)...

Add a section clarifying that Clause Interleaving also applies to the right-hand side of extension methods

@soronpo
Copy link
Contributor

soronpo commented Sep 9, 2022

Do you mean extension [T](...) def foo[U](...) ?
If so this has already been added, and at the time, the codebase was refactored to deal with arbitrarily many type clauses

I remembered scala/scala3#14451 but now I see it will not be resolved.

Might not actually be that challenging, as the implementation boils down to "modify the parser", since the generated tree is already valid in the rest of the compiler

My guess is that type inference and overloading checks are not that generic. Hoping to be proven wrong.

Also explains how overloading resolutions make the alternatives unattractive
@chrisandrews-ms
Copy link

I'm in favor of accepting too. I can see the utility of this proposal, especially for partial inference of type parameters, a problem that has come up a few times in our Scala 2 codebase. The SIP seems well thought out and uncontroversial.

A couple minor questions:

  1. Am I right in assuming that methods with interleaved clauses wouldn't be callable from Scala 2.13 (using the TASTy reader)? I don't see that as a problem (since it also applies to other Scala 3 features) but it would be good to document that limitation.

  2. I note that there's already an experimental implementation PR. Is that ready to merge if this SIP passes the first vote? It would be good to be able to experiment and look for edge cases.

@Sporarum
Copy link
Contributor Author

1. Am I right in assuming that methods with interleaved clauses _wouldn't_ be callable from Scala 2.13 (using the TASTy reader)? I don't see that as a problem (since it also applies to other Scala 3 features) but it would be good to document that limitation.

That is correct, I'll update the proposal

2. I note that there's already an experimental implementation PR. Is that ready to merge if this SIP passes the first vote? It would be good to be able to experiment and look for edge cases.

The implementation is pretty much ready, yes, but it adds it under experimental, so it will only be available in nightly and with an import, which would give us some time to experiment

@Kordyjan
Copy link
Contributor

Kordyjan commented Sep 27, 2022

I maintain my recommendation that the Committee should vote during the next meeting and accept the proposal.

@soronpo
Copy link
Contributor

soronpo commented Sep 27, 2022

I maintain my recommendation that the Committee should vote during the next meeting and accept the proposal.

Same

@chrisandrews-ms
Copy link

I also recommend to accept

@raulraja
Copy link

I maintain my recommendation that the Committee should vote during the next meeting and accept the proposal.

Also, recommend accepting

@adampauls
Copy link

The committee believes that Named Typed Parameter Application (a future SIP) is the better approach to answer use-cases where adjacent type parameter clauses are useful.

Out of curiosity, why does the committee believe these two answers should be mutually exclusive? They are both available for non-type parameters.

@soronpo
Copy link
Contributor

soronpo commented Oct 2, 2022

The committee believes that Named Typed Parameter Application (a future SIP) is the better approach to answer use-cases where adjacent type parameter clauses are useful.

Out of curiosity, why does the committee believe these two answers should be mutually exclusive? They are both available for non-type parameters.

To prevent API entropy and abuse by "creative" developers. In short, it was easier to get approval for the strict version than the general version. It's always possible to submit the following SIP in the future, if indeed there are good use-cases. Small well-calculated and steady increments are good for languages.

Copy link
Contributor

@odersky odersky left a comment

Choose a reason for hiding this comment

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

I also recommend accept.

@Kordyjan
Copy link
Contributor

The proposal was accepted during our last SIP meeting!

This means that the Committee agrees that the proposal can be implemented as an experimental feature.

@Kordyjan Kordyjan merged commit 3ca173c into scala:main Oct 21, 2022
@Sporarum
Copy link
Contributor Author

Thank you, I'm happy to hear that !
The PR for the feature (already in experimental) can be found here:
scala/scala3#14019
I believe it is ready or almost ready, and therefore requires only review from the compiler team
(And I will address the reviews personally)

DefDef ::= DefSig [‘:’ Type] ‘=’ Expr
DefSig ::= id [DefParamClauses] [DefImplicitClause]
DefParamClauses ::= DefParamClauseChunk {DefParamClauseChunk}
DefParamClauseChunk ::= [DefTypeParamClause] TermOrUsingParamClause {TermOrUsingParamClause}

Choose a reason for hiding this comment

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

does this mean that this is not valid anymore:

  def foo[A](implicit a: A) = ...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, nicely spotted, my intent was for it to be allowed,
but it appears I made a mistake with the grammar

Since the implementation team (which is also myself) can make changes to the proposal, I believe this can fall under this umbrella
I will think about what's the clearest way to fix the grammar

Copy link
Contributor Author

@Sporarum Sporarum Oct 26, 2022

Choose a reason for hiding this comment

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

This should fix it:

  DefDef                 ::=  DefSig [‘:’ Type] ‘=’ Expr
- DefSig                 ::=  id [DefParamClauses] [DefImplicitClause]
+ DefSig                 ::=  id [DefParamClauses] [DefTypeParamClause] [DefImplicitClause]
  DefParamClauses        ::=  DefParamClauseChunk {DefParamClauseChunk}
  DefParamClauseChunk    ::=  [DefTypeParamClause] TermOrUsingParamClause {TermOrUsingParamClause}

There is another quirk of this definition however, it allows to parse def f(a: Int)(b: Int) in two different ways (one vs two DefParamClauseChunk), is this an issue ?

Copy link
Contributor

@julienrf julienrf Oct 27, 2022

Choose a reason for hiding this comment

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

I didn’t have a look at the complete grammar but what you propose does not look correct, what about the following?

DefDef                 ::=  DefSig [‘:’ Type] ‘=’ Expr
DefSig                 ::=  id [DefParamClauses]
DefParamClauses        ::=  DefParamClauseChunk {DefParamClauseChunk}
DefParamClauseChunk    ::=  DefTypeParamClause | TermOrUsingParamClause | DefImplicitClause

Choose a reason for hiding this comment

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

Alas, this allows two consecutive type clauses, or multiple implicit clauses.

here's another option:

DefSig                 ::=  id [DefParamClauses] [DefImplicitClause]
DefParamClauses        ::=  DefParamClauseChunkTypeOpt {DefParamClauseChunkTypeReq}
DefParamClauseChunkTypeOpt    ::=  [DefTypeParamClause] {TermOrUsingParamClause}
DefParamClauseChunkTypeReq    ::=  DefTypeParamClause {TermOrUsingParamClause}

Choose a reason for hiding this comment

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

ah, that doesn't work either... perhaps this:

DefSig                 ::=  id [DefParamClauses] [DefImplicitClause]
DefParamClauses        ::=  [DefTypeParamClause] {DefParamClauseChunk} [TermOrUsingParamClause]
DefParamClauseChunk    ::=  TermOrUsingParamClause {TermOrUsingParamClause} DefTypeParamClause

Copy link
Contributor Author

@Sporarum Sporarum Nov 28, 2022

Choose a reason for hiding this comment

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

This should fix it:

  DefDef                 ::=  DefSig [‘:’ Type] ‘=’ Expr
- DefSig                 ::=  id [DefParamClauses] [DefImplicitClause]
+ DefSig                 ::=  id [DefParamClauses] [DefTypeParamClause] [DefImplicitClause]
  DefParamClauses        ::=  DefParamClauseChunk {DefParamClauseChunk}
  DefParamClauseChunk    ::=  [DefTypeParamClause] TermOrUsingParamClause {TermOrUsingParamClause}

There is another quirk of this definition however, it allows to parse def f(a: Int)(b: Int) in two different ways (one vs two DefParamClauseChunk), is this an issue ?

I didn’t have a look at the complete grammar but what you propose does not look correct

@julienrf Could you expand on this, I can't manage to find a failing example with the above 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.

I talked about it some more with @julienrf and we came to a very easy and clean solution:
Use the pre-"no type currying" syntax, and add an end of line comment that says "and type clauses cannot be adjacent"

Copy link
Contributor Author

@Sporarum Sporarum Nov 29, 2022

Choose a reason for hiding this comment

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

- DefSig                 ::=  id [DefParamClauses] [DefImplicitClause]
+ DefSig                 ::=  id [DefParamClauses] [DefImplicitClause]    -- and two DefTypeParamClause cannot be adjacent
- DefParamClauses        ::=  DefParamClauseChunk {DefParamClauseChunk}
- DefParamClauseChunk    ::=  [DefTypeParamClause] TermOrUsingParamClause {TermOrUsingParamClause}
- TermOrUsingParamClause ::=  DefTermParamClause
-                          |  UsingParamClause
+ DefParamClauses        ::= DefParamClause { DefParamClause }
+ DefParamClause         ::= DefTypeParamClause
+                          | DefTermParamClause
+                          |  UsingParamClause

I think this syntax is easier to understand than the formal one

@julienrf How should I go about updating the proposal ?

Copy link
Contributor

Choose a reason for hiding this comment

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

Please open a PR in this repository with your changes.

DefDef ::= DefSig [‘:’ Type] ‘=’ Expr
DefSig ::= id [DefParamClauses] [DefImplicitClause]
DefParamClauses ::= DefParamClauseChunk {DefParamClauseChunk}
DefParamClauseChunk ::= [DefTypeParamClause] TermOrUsingParamClause {TermOrUsingParamClause}

Choose a reason for hiding this comment

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

Alas, this allows two consecutive type clauses, or multiple implicit clauses.

here's another option:

DefSig                 ::=  id [DefParamClauses] [DefImplicitClause]
DefParamClauses        ::=  DefParamClauseChunkTypeOpt {DefParamClauseChunkTypeReq}
DefParamClauseChunkTypeOpt    ::=  [DefTypeParamClause] {TermOrUsingParamClause}
DefParamClauseChunkTypeReq    ::=  DefTypeParamClause {TermOrUsingParamClause}

Comment on lines +118 to +119
UsingParamClause ::= [nl] ‘(’ ‘using’ (DefTermParams | FunArgTypes) ‘)’
DefImplicitClause ::= [nl] ‘(’ ‘implicit’ DefTermParams ‘)’

Choose a reason for hiding this comment

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

I'd also rename

TermOrUsingParamClause ::=  DefTermParamClause
                         |  UsingParamClause
UsingParamClause       ::=  [nl] ‘(’ ‘using’ (DefTermParams | FunArgTypes) ‘)’
DefImplicitClause      ::=  [nl] ‘(’ ‘implicit’ DefTermParams ‘)’

to

DefNonTypeParamClause  ::=  DefTermParamClause |  DefUsingParamClause
DefUsingParamClause    ::=  [nl] ‘(’ ‘using’ (DefTermParams | FunArgTypes) ‘)’
DefImplicitParamClause ::=  [nl] ‘(’ ‘implicit’ DefTermParams ‘)’

Sporarum added a commit to Sporarum/improvement-proposals that referenced this pull request Nov 29, 2022
As noted in scala#47, the grammar did not correspond to the rest of the proposal, this PR solves that
Sporarum pushed a commit to scala/scala3 that referenced this pull request Feb 3, 2023
This aims to add the ability to declare functions with many clauses of
type parameters, instead of at most one, and to allow those clauses to
be interleaved with term clauses:
```scala
def foo[A](x: A)[B](y: B) = (x, y)
```
All user-facing details can be found in the [Scala 3 new features
doc](https://github.com/lampepfl/dotty/blob/2a079f92bfc05420e90987d908c41589ac94418d/docs/_docs/reference/other-new-features/generalized-method-syntax.md),
and in the [SIP
proposal](scala/improvement-proposals#47)

The implementation details are described below in the commit messages

The community's opinion of the feature can be found on [the scala
contributors
forum](https://contributors.scala-lang.org/t/clause-interweaving-allowing-def-f-t-x-t-u-y-u)
(note however that the description there is somewhat outdated)

### Dependencies

This feature has been [accepted by the SIP committee for
implementation](scala/improvement-proposals#47 (comment)),
it can therefore become part of the language as an experimental feature
at any time.

The feature will be available with `import
scala.language.experimental.clauseInterleaving`

### How to make non-experimental
`git revert` the commits named "Make clause interleaving experimental"
and "Add import to tests"

### Future Work
1. Implement given aliases with clause interweaving: (to have types
depends on terms)
```scala
given myGiven[T](using x: T)[U](using y: U) = (x, y)
```
2. Add interleaved clauses to the left-hand side of extension methods:
```scala
extension (using Int)[A](using A)(a: A)[B](using B)
  def foo: (A, B) = ???
```
3. Investigate usefulness/details of clause interweaving for classes and
type currying for types:
```scala
class Foo[A](a: A)[B](b: B)
new Foo(0)("Hello!") // type: Foo[Int][String] ?

type Bar[A][B] = Map[A, B]
Bar[Char] // should this mean [B] =>> Bar[Char][B] ?
```

(Started as a semester project with supervision from @smarter, now part
of my missions as an intern at the scala center)
odersky pushed a commit to odersky/improvement-proposals that referenced this pull request Jul 18, 2024
As noted in scala#47, the grammar did not correspond to the rest of the proposal, this PR solves that
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.