Skip to content

Commit 567d760

Browse files
committed
Simplify syntax of givens defining extension methods
- All parameters go in the front - remainde consists only of methods - indentation is significant Example: ```scala given [T](xs: List[T]) def second = xs.tail.head def third = xs.tail.tail.head ```
1 parent 80e6432 commit 567d760

File tree

5 files changed

+188
-13
lines changed

5 files changed

+188
-13
lines changed

compiler/src/dotty/tools/dotc/parsing/Parsers.scala

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -125,13 +125,13 @@ object Parsers {
125125

126126
/* ------------- ERROR HANDLING ------------------------------------------- */
127127
/** The offset where the last syntax error was reported, or if a skip to a
128-
* safepoint occurred afterwards, the offset of the safe point.
129-
*/
128+
* safepoint occurred afterwards, the offset of the safe point.
129+
*/
130130
protected var lastErrorOffset : Int = -1
131131

132132
/** Issue an error at given offset if beyond last error offset
133-
* and update lastErrorOffset.
134-
*/
133+
* and update lastErrorOffset.
134+
*/
135135
def syntaxError(msg: => Message, offset: Int = in.offset): Unit =
136136
if (offset > lastErrorOffset) {
137137
val length = if (offset == in.offset && in.name != null) in.name.show.length else 0
@@ -3360,12 +3360,20 @@ object Parsers {
33603360
case _ =>
33613361
syntaxError(em"extension clause must start with a single regular parameter", start)
33623362

3363+
def checkExtensionMethod(stat: Tree): Unit = stat match {
3364+
case stat: DefDef =>
3365+
if stat.mods.is(Extension) then
3366+
syntaxError(i"no extension method allowed here since leading parameter was already given", stat.span)
3367+
case _ =>
3368+
syntaxError(i"extension clause can only define methods", stat.span)
3369+
}
33633370

33643371
/** GivenDef ::= [GivenSig (‘:’ | <:)] Type ‘=’ Expr
33653372
* | [GivenSig ‘:’] [ConstrApp {‘,’ ConstrApp }] [TemplateBody]
3366-
* | [id ‘:’] [ExtParamClause] TemplateBody
3373+
* | [id ‘:’] ExtParamClause ExtMethods
33673374
* GivenSig ::= [id] [DefTypeParamClause] {GivenParamClause}
33683375
* ExtParamClause ::= [DefTypeParamClause] DefParamClause {GivenParamClause}
3376+
* ExtMethods ::= [nl] ‘{’ ‘def’ DefDef {semi ‘def’ DefDef} ‘}’
33693377
*/
33703378
def givenDef(start: Offset, mods: Modifiers, instanceMod: Mod) = atSpan(start, nameStart) {
33713379
var mods1 = addMod(mods, instanceMod)
@@ -3407,8 +3415,10 @@ object Parsers {
34073415
syntaxError("`<:' is only allowed for given with `inline' modifier")
34083416
in.nextToken()
34093417
TypeBoundsTree(EmptyTree, toplevelTyp()) :: Nil
3410-
else if name.isEmpty && in.token != LBRACE && in.token != WITH then
3411-
tokenSeparated(COMMA, constrApp)
3418+
else if name.isEmpty
3419+
&& in.token != LBRACE && in.token != WITH
3420+
&& !hasExtensionParams
3421+
then tokenSeparated(COMMA, constrApp)
34123422
else Nil
34133423

34143424
val gdef =
@@ -3421,12 +3431,17 @@ object Parsers {
34213431
case TypeBoundsTree(_, _) :: _ => syntaxError("`=' expected")
34223432
case _ =>
34233433
possibleTemplateStart()
3424-
if !hasExtensionParams then
3434+
if hasExtensionParams then
3435+
in.observeIndented()
3436+
else
34253437
tparams = tparams.map(tparam => tparam.withMods(tparam.mods | PrivateLocal))
34263438
vparamss = vparamss.map(_.map(vparam =>
34273439
vparam.withMods(vparam.mods &~ Param | ParamAccessor | PrivateLocal)))
34283440
val templ = templateBodyOpt(makeConstructor(tparams, vparamss), parents, Nil)
3429-
if tparams.isEmpty && vparamss.isEmpty || hasExtensionParams then ModuleDef(name, templ)
3441+
if hasExtensionParams then
3442+
templ.body.foreach(checkExtensionMethod)
3443+
ModuleDef(name, templ)
3444+
else if tparams.isEmpty && vparamss.isEmpty then ModuleDef(name, templ)
34303445
else TypeDef(name.toTypeName, templ)
34313446

34323447
finalizeDef(gdef, mods1, start)

docs/docs/internals/syntax.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,9 +388,10 @@ ObjectDef ::= id [Template]
388388
EnumDef ::= id ClassConstr InheritClauses EnumBody EnumDef(mods, name, tparams, template)
389389
GivenDef ::= [GivenSig (‘:’ | <:)] Type ‘=’ Expr
390390
| [GivenSig ‘:’] [ConstrApp {‘,’ ConstrApp }] [TemplateBody]
391-
| [GivenSig ‘:’] [ExtParamClause] TemplateBody
391+
| [GivenSig ‘:’] ExtParamClause ExtMethods
392392
GivenSig ::= [id] [DefTypeParamClause] {GivenParamClause}
393393
ExtParamClause ::= [DefTypeParamClause] ‘(’ DefParam ‘)’ {GivenParamClause}
394+
ExtMethods ::= [nl] ‘{’ ‘def’ DefDef {semi ‘def’ DefDef} ‘}’
394395
Template ::= InheritClauses [TemplateBody] Template(constr, parents, self, stats)
395396
InheritClauses ::= [‘extends’ ConstrApps] [‘derives’ QualId {‘,’ QualId}]
396397
ConstrApps ::= ConstrApp {(‘,’ | ‘with’) ConstrApp}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
---
2+
layout: doc-page
3+
title: "Extension Methods"
4+
---
5+
6+
Extension methods allow one to add methods to a type after the type is defined. Example:
7+
8+
```scala
9+
case class Circle(x: Double, y: Double, radius: Double)
10+
11+
def (c: Circle) circumference: Double = c.radius * math.Pi * 2
12+
```
13+
14+
Like regular methods, extension methods can be invoked with infix `.`:
15+
16+
```scala
17+
val circle = Circle(0, 0, 1)
18+
circle.circumference
19+
```
20+
21+
### Translation of Extension Methods
22+
23+
Extension methods are methods that have a parameter clause in front of the defined
24+
identifier. They translate to methods where the leading parameter section is moved
25+
to after the defined identifier. So, the definition of `circumference` above translates
26+
to the plain method, and can also be invoked as such:
27+
```scala
28+
def circumference(c: Circle): Double = c.radius * math.Pi * 2
29+
30+
assert(circle.circumference == circumference(circle))
31+
```
32+
33+
### Translation of Calls to Extension Methods
34+
35+
When is an extension method applicable? There are two possibilities.
36+
37+
- An extension method is applicable if it is visible under a simple name, by being defined
38+
or inherited or imported in a scope enclosing the application.
39+
- An extension method is applicable if it is a member of some given instance at the point of the application.
40+
41+
As an example, consider an extension method `longestStrings` on `Seq[String]` defined in a trait `StringSeqOps`.
42+
43+
```scala
44+
trait StringSeqOps {
45+
def (xs: Seq[String]) longestStrings = {
46+
val maxLength = xs.map(_.length).max
47+
xs.filter(_.length == maxLength)
48+
}
49+
}
50+
```
51+
We can make the extension method available by defining a given `StringSeqOps` instance, like this:
52+
```scala
53+
given ops1: StringSeqOps
54+
```
55+
Then
56+
```scala
57+
List("here", "is", "a", "list").longestStrings
58+
```
59+
is legal everywhere `ops1` is available. Alternatively, we can define `longestStrings` as a member of a normal object. But then the method has to be brought into scope to be usable as an extension method.
60+
61+
```scala
62+
object ops2 extends StringSeqOps
63+
import ops2.longestStrings
64+
List("here", "is", "a", "list").longestStrings
65+
```
66+
The precise rules for resolving a selection to an extension method are as follows.
67+
68+
Assume a selection `e.m[Ts]` where `m` is not a member of `e`, where the type arguments `[Ts]` are optional,
69+
and where `T` is the expected type. The following two rewritings are tried in order:
70+
71+
1. The selection is rewritten to `m[Ts](e)`.
72+
2. If the first rewriting does not typecheck with expected type `T`, and there is a given instance `i`
73+
in either the current scope or in the implicit scope of `T`, and `i` defines an extension
74+
method named `m`, then selection is expanded to `i.m[Ts](e)`.
75+
This second rewriting is attempted at the time where the compiler also tries an implicit conversion
76+
from `T` to a type containing `m`. If there is more than one way of rewriting, an ambiguity error results.
77+
78+
So `circle.circumference` translates to `CircleOps.circumference(circle)`, provided
79+
`circle` has type `Circle` and `CircleOps` is given (i.e. it is visible at the point of call or it is defined in the companion object of `Circle`).
80+
81+
### Given Instances for Extension Methods
82+
83+
A special syntax allows to define a given instance for one or more extension methods without listing a parent type.
84+
Examples:
85+
86+
```scala
87+
given stringOps: (xs: Seq[String]) {
88+
def longestStrings: Seq[String] = {
89+
val maxLength = xs.map(_.length).max
90+
xs.filter(_.length == maxLength)
91+
}
92+
}
93+
94+
given [T](xs: List[T]) {
95+
def second = xs.tail.head
96+
def third[T]: T = xs.tail.tail.head
97+
}
98+
```
99+
These given clauses define extension methods `longestStrings`, `second`, and `third`. All extension methods defined in such a given clause
100+
share the same leading parameters, which follow the `given`. The remainder of the extension methods is written as regular defs inside braces.
101+
102+
If such given instances are anonymous (as in the second clause), their name is synthesized from the name of the first defined extension method.
103+
104+
105+
### Operators
106+
107+
The extension method syntax also applies to the definition of operators.
108+
In each case the definition syntax mirrors the way the operator is applied.
109+
Examples:
110+
```scala
111+
def (x: String) < (y: String) = ...
112+
def (x: Elem) +: (xs: Seq[Elem]) = ...
113+
114+
"ab" + "c"
115+
1 +: List(2, 3)
116+
```
117+
The two definitions above translate to
118+
```scala
119+
def < (x: String)(y: String) = ...
120+
def +: (xs: Seq[Elem])(x: Elem) = ...
121+
```
122+
Note that swap of the two parameters `x` and `xs` when translating
123+
the right-binding operator `+:` to an extension method. This is analogous
124+
to the implementation of right binding operators as normal methods.
125+
126+
### Generic Extensions
127+
128+
The `StringSeqOps` examples extended a specific instance of a generic type. It is also possible to extend a generic type by adding type parameters to an extension method. Examples:
129+
130+
```scala
131+
def (xs: List[T]) second [T] =
132+
xs.tail.head
133+
134+
def (xs: List[List[T]]) flattened [T] =
135+
xs.foldLeft[List[T]](Nil)(_ ++ _)
136+
137+
def (x: T) + [T : Numeric](y: T): T =
138+
summon[Numeric[T]].plus(x, y)
139+
```
140+
141+
As usual, type parameters of the extension method follow the defined method name. Nevertheless, such type parameters can already be used in the preceding parameter clause.
142+
143+
144+
### Syntax
145+
146+
The required syntax extension just adds one clause for extension methods relative
147+
to the [current syntax](../../internals/syntax.md).
148+
```
149+
DefSig ::= ...
150+
| ‘(’ DefParam ‘)’ [nl] id [DefTypeParamClause] DefParamClauses
151+
GivenDef ::= ...
152+
[GivenSig ‘:’] [ExtParamClause] ExtMethods
153+
ExtParamClause ::= [DefTypeParamClause] ‘(’ DefParam ‘)’ {GivenParamClause}
154+
ExtMethods ::= [nl] ‘{’ ‘def’ DefDef {semi ‘def’ DefDef} ‘}’
155+
```

docs/docs/reference/contextual/extension-methods.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ layout: doc-page
33
title: "Extension Methods"
44
---
55

6+
**Note** The syntax described in this section is currently under revision.
7+
[Here is the new version which will be implemented in Dotty 0.20](./extension-methods-new.html).
8+
69
Extension methods allow one to add methods to a type after the type is defined. Example:
710

811
```scala

tests/pos/reference/delegates.scala

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,14 @@ object Instances extends Common with
3939
if (fst != 0) fst else xs1.compareTo(ys1)
4040
end listOrd
4141

42-
given stringOps: (xs: Seq[String]) with
42+
given stringOps: (xs: Seq[String])
4343
def longestStrings: Seq[String] =
4444
val maxLength = xs.map(_.length).max
4545
xs.filter(_.length == maxLength)
4646

47-
given [T](xs: List[T]) with
47+
given [T](xs: List[T])
4848
def second = xs.tail.head
49+
def third = xs.tail.tail.head
4950

5051
given listMonad: Monad[List] with
5152
def (xs: List[A]) flatMap[A, B] (f: A => List[B]): List[B] =
@@ -132,7 +133,7 @@ object PostConditions with
132133

133134
def result[T](given x: WrappedResult[T]): T = x
134135

135-
given [T](x: T) with
136+
given [T](x: T)
136137
def ensuring(condition: (given WrappedResult[T]) => Boolean): T =
137138
assert(condition(given x))
138139
x

0 commit comments

Comments
 (0)