Skip to content

Commit 7290a28

Browse files
Update SIPs state
1 parent 81b05e2 commit 7290a28

File tree

4 files changed

+888
-16
lines changed

4 files changed

+888
-16
lines changed

_sips/sips/clause-interleaving.md

Lines changed: 177 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,180 @@
11
---
2+
layout: sip
23
title: SIP-47 - Clause Interleaving
3-
status: vote-requested
4-
pull-request-number: 47
5-
stage: design
6-
recommendation: accept
7-
4+
stage: implementation
5+
status: waiting-for-implementation
6+
permalink: /sips/:title.html
87
---
8+
9+
**By: Quentin Bernet and Guillaume Martres and Sébastien Doeraene**
10+
11+
## History
12+
13+
| Date | Version |
14+
|---------------|-----------------------|
15+
| May 5th 2022 | Initial Draft |
16+
| Aug 17th 2022 | Formatting |
17+
| Sep 22th 2022 | Type Currying removed |
18+
19+
## Summary
20+
21+
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
22+
~~~ scala
23+
def pair[A](a: A)[B](b: B): (A, B) = (a, b)
24+
~~~
25+
Here is also a more complicated and contrived example that highlights all the possible interactions:
26+
~~~ scala
27+
def foo[A](using a: A)(b: List[A])[C <: a.type, D](cd: (C, D))[E]: Foo[A, B, C, D, E]
28+
~~~
29+
30+
31+
## Motivation
32+
33+
We motivate the feature with two use cases:
34+
35+
* a `getOrElse` method for a heterogeneous key-value store, which is an occurrence of wanting a type parameter whose bounds are path-dependent on a term parameter, and
36+
37+
### Heterogeneous key-value store
38+
Consider an API for a heterogenous key-value store, where keys know what type of value they must be associated to:
39+
~~~ scala
40+
trait Key:
41+
type Value
42+
43+
class Store:
44+
def get(key: Key): key.Value =
45+
def put(key: Key)(value: => key.Value): Unit =
46+
~~~
47+
We want to provide a method `getOrElse`, taking a default value to be used if the key is not present. Such a method could look like
48+
~~~ scala
49+
def getOrElse(key: Key)(default: => key.Value): key.Value =
50+
~~~
51+
However, at call site, it would prevent from using as default value a value that is not a valid `key.Value`. This is a limitation compared to other `getOrElse`-style methods such as that of `Option`, which allow passing any supertype of the element type.
52+
53+
In current Scala, there is no way to define `Store.getOrElse` in a way that supports this use case. We may try to define it as
54+
~~~ scala
55+
def getOrElse[V >: key.Value](key: Key)(default: => V): V =
56+
~~~
57+
but that is not valid because the declaration of `V` needs to refer to the path-dependent type `key.Value`, which is defined in a later parameter list.
58+
59+
We might also try to move the type parameter list after `key` to avoid that problem, as
60+
~~~ scala
61+
def getOrElse(key: Key)[V >: key.Value](default: => V): V =
62+
~~~
63+
but that is also invalid because type parameter lists must always come first.
64+
65+
A workaround is to return an intermediate object with an `apply` method, as follows:
66+
~~~ scala
67+
class Store:
68+
final class StoreGetOrElse[K <: Key](val key: K):
69+
def apply[V >: key.Value](default: => V): V =
70+
def getOrElse(key: Key): StoreGetOrElse[key.type] = StoreGetOrElse(key)
71+
~~~
72+
This definition provides the expected source API at call site, but it has two issues:
73+
* It is more complex than expected, forcing a user looking at the API to navigate to the definition of `StoreGetOrElse` to make sense of it.
74+
* It is inefficient, as an intermediate instance of `StoreGetOrElse` must be created for each call to `getOrElse`.
75+
* Overloading resolution looks at clauses after the first one, but only in methods, the above is ambiguous with any `def getOrElse(k:Key): ...`, whereas the proposed signature is not ambiguous with for example `def getOrElse(k:Key)[A,B](x: A, y: B)`
76+
77+
Another workaround is to return a polymorphic function, for example:
78+
~~~scala
79+
def getOrElse(k:Key): [V >: k.Value] => (default: V) => V =
80+
[V] => (default: V) => ???
81+
~~~
82+
While again, this provides the expected API at call site, it also has issues:
83+
* The behavior is not the same, as `default` has to be a by-value parameter
84+
* The definition is hard to visually parse, as users are more used to methods (and it is our opinion this should remain so)
85+
* The definition is cumbersome to write, especially if there are a lot of term parameters
86+
* Methods containing curried type clauses like `def foo[A][B](x: B)` cannot be represented in this way, as polymorphic methods always have to have a term parameter right after.
87+
* It is inefficient, as many closures must be created for each call to `getOrElse` (one per term clause to the right of the first non-initial type clause).
88+
* Same problem as above with overloading
89+
90+
## Proposed solution
91+
### High-level overview
92+
93+
To solve the above problems, we propose to generalize method signatures so that they can have multiple type parameter lists, interleaved with term parameter lists and using parameter lists.
94+
95+
For the heterogeneous key-value store example, this allows to define `getOrElse` as follows:
96+
~~~ scala
97+
def getOrElse(key: Key)[V >: key.Value](default: => V): V =
98+
~~~
99+
It provides the best of all worlds:
100+
* A convenient API at call site
101+
* A single point of documentation
102+
* Efficiency, since the method erases to a single JVM method with signature `getOrElse(Object,Object)Object`
103+
104+
### Specification
105+
We amend the syntax of def parameter clauses as follows:
106+
107+
~~~
108+
DefDcl ::= DefSig ‘:’ Type
109+
DefDef ::= DefSig [‘:’ Type] ‘=’ Expr
110+
DefSig ::= id [DefParamClauses] [DefImplicitClause]
111+
DefParamClauses ::= DefParamClauseChunk {DefParamClauseChunk}
112+
DefParamClauseChunk ::= [DefTypeParamClause] TermOrUsingParamClause {TermOrUsingParamClause}
113+
TermOrUsingParamClause ::= DefTermParamClause
114+
| UsingParamClause
115+
DefTypeParamClause ::= [nl] ‘[’ DefTypeParam {‘,’ DefTypeParam} ‘]’
116+
DefTypeParam ::= {Annotation} id [HkTypeParamClause] TypeParamBounds
117+
DefTermParamClause ::= [nl] ‘(’ [DefTermParams] ‘)’
118+
UsingParamClause ::= [nl] ‘(’ ‘using’ (DefTermParams | FunArgTypes) ‘)’
119+
DefImplicitClause ::= [nl] ‘(’ ‘implicit’ DefTermParams ‘)’
120+
DefTermParams ::= DefTermParam {‘,’ DefTermParam}
121+
DefTermParam ::= {Annotation} [‘inline’] Param
122+
Param ::= id ‘:’ ParamType [‘=’ Expr]
123+
~~~
124+
125+
The main rules of interest are `DefParamClauses` and `DefParamClauseChunk`, which now allow any number of type parameter clauses, term parameter clauses and using parameter clauses, in any order as long as there are no two adjacent type clauses.
126+
127+
Note that these are also used for the right-hand side of extension methods, clause interleaving thus also applies to them.
128+
129+
It is worth pointing out that there can still only be at most one implicit parameter clause, which, if present, must be at the end.
130+
131+
The type system and semantics naturally generalize to these new method signatures.
132+
133+
### Restrictions
134+
135+
#### Type Currying
136+
Currying type clauses enables partial type inference, as the left clause can be specified while the right one is not.
137+
As this is a very useful feature, we expect people would use it liberally, and recommending the curried form.
138+
We are uncertain about the readability of the resulting methods, we have therefore decided to not include type currying as part of this proposal.
139+
140+
Note however that, if absolutely necessary, it is still possible to curry type parameters as such: `def foo[A](using DummyImplicit)[B]`, since the implicit search for `DummyImplicit` will always succeed.
141+
This is sufficiently unwieldy that it is unlikely the above becomes the norm.
142+
143+
#### Class Signatures
144+
Class signatures are unchanged. Classes can still only have at most one type parameter list, which must come first. For example, the following definition is still invalid:
145+
~~~ scala
146+
class Pair[+A](val a: A)[+B](val b: B)
147+
~~~
148+
Class signatures already have limitations compared to def signatures. For example, they must have at least one term parameter list. There is therefore precedent for limiting their expressiveness compared to def parameter lists.
149+
150+
The rationale for this restriction is that classes also define associated types. It is unclear what the type of an instance of `Pair` with `A` and `B` should be. It could be defined as `Foo[A][B]`. That still leaves holes in terms of path-dependent types, as `B`'s definition could not depend on the path `a`. Allowing interleaved type parameters for class definitions is therefore restricted for now. It could be allowed with a follow-up proposal.
151+
152+
Note: As `apply` is a normal method, it is totally possible to define a method `def apply[A](a: A)[B](b: B)` on `Pair`'s companion object, allowing to create instances with `Pair[Int](4)[Char]('c')`.
153+
154+
#### LHS of extension methods
155+
The left hand side of extension methods remains unchanged, since they only have one explicit term clause, and since the type parameters are very rarely passed explicitly, it is not as necessary to have multiple type clauses there.
156+
157+
Currently, Scala 2 can only call/override methods with at most one leading type parameter clause, which already forbids calling extension methods like `extension (x: Int) def bar[A](y: A)`, which desugars to `def bar(x: Int)[A](y: A)`. This proposal does not change this, so methods like `def foo[A](x: A)[B]` will not be callable from Scala 2.
158+
159+
### Compatibility
160+
The proposal is expected to be backward source compatible. New signatures currently do not parse, and typing rules are unchanged for existing signatures.
161+
162+
Backward binary compatibility is straightforward.
163+
164+
Backward TASTy compatibility should be straightforward. The TASTy format is such that we can extend it to support interleaved type parameter lists without added complexity. If necessary, a version check can decide whether to read signatures in the new or old format. For typechecking, like for source compatibility, the typing rules are unchanged for signatures that were valid before.
165+
166+
Of course, libraries that choose to evolve their public API to take advantage of the new signatures may expose incompatibilities.
167+
168+
## Alternatives
169+
The proposal is a natural generalization of method signatures.
170+
We could have extended the proposal to type currying (allowing partial type inference), but have not due to the concerns mentionned in [Restrictions](#restrictions).
171+
This might be the subject of a follow up proposal, if the concerns can be addressed.
172+
173+
As discussed above, we may want to consider generalizing class parameter lists as well. However, we feel it is better to leave that extension to a follow-up proposal, if required.
174+
175+
## Related work
176+
* Pre-SIP: https://contributors.scala-lang.org/t/clause-interweaving-allowing-def-f-t-x-t-u-y-u/5525
177+
* An implementation of the proposal is available as a pull request at https://github.com/lampepfl/dotty/pull/14019
178+
179+
## FAQ
180+
Currently empty.

_sips/sips/fewer-braces.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
---
22
layout: sip
33
permalink: /sips/:title.html
4-
stage: implementation
5-
status: vote-requested
6-
recommendation: accept
4+
stage: completed
5+
status: accepted
76
title: SIP-44 - Fewer Braces
87
---
98

0 commit comments

Comments
 (0)