Skip to content

Commit 5dedefa

Browse files
committed
Schema for named extensions
Following suggestions by @sjrd, a worked out scheme to have named extensions, using extension <name> for <type> { ... } syntax.
1 parent a3fa2b2 commit 5dedefa

File tree

4 files changed

+303
-4
lines changed

4 files changed

+303
-4
lines changed

docs/docs/internals/syntax.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -330,12 +330,18 @@ DefDef ::= DefSig [‘:’ Type] ‘=’ Expr
330330
TmplDef ::= ([‘case’] ‘class’ | trait’) ClassDef
331331
| [‘case’] ‘object’ ObjectDef
332332
| `enum' EnumDef
333-
ClassDef ::= id ClassConstr TemplateOpt ClassDef(mods, name, tparams, templ)
333+
ClassDef ::= id ClassConstr [TemplateClause] ClassDef(mods, name, tparams, templ)
334334
ClassConstr ::= [ClsTypeParamClause] [ConstrMods] ClsParamClauses with DefDef(_, <init>, Nil, vparamss, EmptyTree, EmptyTree) as first stat
335335
ConstrMods ::= {Annotation} [AccessModifier]
336-
ObjectDef ::= id TemplateOpt ModuleDef(mods, name, template) // no constructor
337-
EnumDef ::= id ClassConstr [`extends' [ConstrApps]] EnumBody EnumDef(mods, name, tparams, template)
338-
TemplateOpt ::= [‘extends’ Template | [nl] TemplateBody]
336+
ObjectDef ::= id [TemplateClause] ModuleDef(mods, name, template) // no constructor
337+
EnumDef ::= id ClassConstr [`extends' [ConstrApps]] EnumBody TypeDef(mods, name, template)
338+
Extension ::= 'extension' id [ExtensionParams] Extension(name, templ)
339+
'for' Type ExtensionClause
340+
ExtensionParams ::= [ClsTypeParamClause] [[nl] ImplicitParamClause]
341+
ExtensionClause ::= [`:` Template]
342+
| [nl] ‘{’ ‘def’ DefDef {semi ‘def’ DefDef} ‘}’
343+
344+
TemplateClause ::= [‘extends’ Template | [nl] TemplateBody]
339345
Template ::= ConstrApps [TemplateBody] | TemplateBody Template(constr, parents, self, stats)
340346
ConstrApps ::= ConstrApp {‘with’ ConstrApp}
341347
ConstrApp ::= AnnotType {ArgumentExprs} Apply(tp, args)
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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+
extension CircleOps for Circle {
12+
def circumference: Double = this.radius * math.Pi * 2
13+
}
14+
```
15+
16+
The extension adds a method `circumference` to values of class `Circle`. Like regular methods, extension methods can be invoked with infix `.`:
17+
18+
```scala
19+
val circle = Circle(0, 0, 1)
20+
circle.circumference
21+
```
22+
23+
### Meaning of `this`
24+
25+
Inside an extension method, the name `this` stands for the receiver on which the
26+
method is applied when it is invoked. E.g. in the application of `circle.circumference`,
27+
the `this` in the body of `circumference` refers to `circle`. As usual, `this` can be elided, so we could also have defined `CircleOps` like this:
28+
29+
```scala
30+
extension CircleOps for Circle {
31+
def circumference: Double = radius * math.Pi * 2
32+
}
33+
```
34+
35+
### Scope of Extensions
36+
37+
Extensions can appear anywhere in a program; there is no need to co-define them with the types they extend. Extension methods are available wherever their defining extension is in scope. Extensions can be inherited or imported like normal definitions.
38+
39+
### Extended Types
40+
41+
An extension can add methods to arbitrary types. For instance, the following
42+
clause adds a `longestStrings` extension method to a `Seq[String]`:
43+
44+
```scala
45+
extension StringOps for Seq[String] {
46+
def longestStrings: Seq[String] = {
47+
val maxLength = map(_.length).max
48+
filter(_.length == maxLength)
49+
}
50+
}
51+
```
52+
53+
### Generic Extensions
54+
55+
The previous example extended a specific instance of a generic type. It is also possible
56+
to extend a generic type by adding type parameters to an extension:
57+
58+
```scala
59+
extension ListOps[T] for List[T] {
60+
def second: T = tail.head
61+
}
62+
```
63+
64+
or:
65+
66+
67+
```scala
68+
extension ListListOps[T] for List[List[T]] {
69+
def flattened: List[T] = foldLeft[List[T]](Nil)(_ ++ _)
70+
}
71+
```
72+
73+
### Bounded Generic Extensions
74+
75+
It is possible to use bounds for the type parameters of an extension. But subtype and context bounds are supported:
76+
77+
```scala
78+
extension ShapeListOps[T <: Shape] for List[T] {
79+
def totalArea = map(_.area).sum
80+
}
81+
82+
extension OrdSeqOps[T : math.Ordering] for Seq[T] {
83+
def indexOfLargest = zipWithIndex.maxBy(_._1)._2
84+
def indexOfSmallest = zipWithIndex.minBy(_._1)._2
85+
}
86+
```
87+
88+
### Implicit Parameters for Type Patterns
89+
90+
The standard translation of context bounds expands the bound in the last example to an implicit _evidence_ parameter of type `math.Ordering[T]`. It is also possible to give evidence parameters explicitly. The following example is equivalent to the previous one:
91+
92+
```scala
93+
extension OrdSeqOps[T](implicit ev: math.Ordering[T]) for Seq[T] {
94+
def indexOfLargest = this.zipWithIndex.maxBy(_._1)._2
95+
def indexOfSmallest = this.zipWithIndex.minBy(_._1)._2
96+
}
97+
```
98+
99+
There can be only one parameter clause following a type pattern and it must be implicit. As usual, one can combine context bounds and implicit evidence parameters.
100+
101+
### Toplevel Type Variables
102+
103+
A type pattern consisting of a top-level typevariable introduces a fully generic extension. For instance, the following extension introduces `x ~ y` as an alias
104+
for `(x, y)`:
105+
106+
```scala
107+
extension InfixPair[T] for T {
108+
def ~ [U](that: U) = (this, that)
109+
}
110+
```
111+
112+
As a larger example, here is a way to define constructs for checking arbitrary postconditions using `ensuring` so that the checked result can be referred to simply by `result`. The example combines opaque aliases, implicit function types, and extensions to provide a zero-overhead abstraction.
113+
114+
```scala
115+
object PostConditions {
116+
opaque type WrappedResult[T] = T
117+
118+
private object WrappedResult {
119+
def wrap[T](x: T): WrappedResult[T] = x
120+
def unwrap[T](x: WrappedResult[T]): T = x
121+
}
122+
123+
def result[T](implicit er: WrappedResult[T]): T = WrappedResult.unwrap(er)
124+
125+
extenson Ensuring[T] for T {
126+
def ensuring(condition: implicit WrappedResult[T] => Boolean): T = {
127+
implicit val wrapped = WrappedResult.wrap(this)
128+
assert(condition)
129+
this
130+
}
131+
}
132+
}
133+
object Test {
134+
import PostConditions._
135+
val s = List(1, 2, 3).sum.ensuring(result == 6)
136+
}
137+
```
138+
**Explanations**: We use an implicit function type `implicit WrappedResult[T] => Boolean`
139+
as the type of the condition of `ensuring`. An argument condition to `ensuring` such as
140+
`(result == 6)` will therefore have an implicit value of type `WrappedResult[T]` in scope
141+
to pass along to the `result` method. `WrappedResult` is a fresh type, to make sure that we do not get unwanted implicits in scope (this is good practice in all cases where implicit parameters are involved). Since `WrappedResult` is an opaque type alias, its values need not be boxed, and since `ensuring` is added as an extension method, its argument does not need boxing either. Hence, the implementation of `ensuring` is as about as efficient as the best possible code one could write by hand:
142+
143+
{ val result = List(1, 2, 3).sum
144+
assert(result == 6)
145+
result
146+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
---
2+
layout: doc-page
3+
title: "Instance Declarations"
4+
---
5+
6+
In addition to adding methods, an extension can also implement traits. Extensions implementing traits are also called _instance declarations_. For example,
7+
8+
```scala
9+
trait HasArea {
10+
def area: Double
11+
}
12+
13+
extension CircleHasArea for Circle : HasArea {
14+
def area = this.radius * this.radius * math.Pi
15+
}
16+
```
17+
18+
This extension makes `Circle` an instance of the `HasArea` trait. Specifically, it defines an implicit subclass of `HasArea`
19+
which takes a `Circle` as argument and provides the given implementation. Hence, the implementation of the extension above would be like this
20+
21+
```scala
22+
implicit class CircleHasArea($this: Circle) extends HasArea {
23+
def area = $this.radius * $this.radius * math.Pi
24+
}
25+
```
26+
27+
An instance definition can thus provide a kind of "implements" relationship that can be defined independently of the types it connects.
28+
29+
### Generic Instance Declarations
30+
31+
Just like extension methods, instance declarations can also be generic and their type parameters can have bounds.
32+
33+
For example, assume we have the following two traits, which define binary and unary (infix) equality tests:
34+
35+
```scala
36+
trait Eql[T] {
37+
def eql (x: T, y: T): Boolean
38+
}
39+
40+
trait HasEql[T] {
41+
def === (that: T): Boolean
42+
}
43+
```
44+
45+
The following extension makes any type `T` with an implicit `Eql[T]` instance implement `HasEql`:
46+
47+
```scala
48+
extension HasEqlImpl[T : Eql] for T : HasEql[T] {
49+
def === (that: T): Boolean = implicitly[Eql[T]].eql(this, that)
50+
}
51+
```
52+
53+
### Syntax of Extensions
54+
55+
The syntax of extensions is specified below as a delta with respect to the Scala syntax given [here](http://dotty.epfl.ch/docs/internals/syntax.html)
56+
57+
TmplDef ::= ...
58+
| ‘extension’ ExtensionDef
59+
ExtensionDef ::= id [ExtensionParams] 'for' Type ExtensionClause
60+
ExtensionParams ::= [ClsTypeParamClause] [[nl] ImplicitParamClause]
61+
ExtensionClause ::= [`:` Template]
62+
| [nl] ‘{’ ‘def’ DefDef {semi ‘def’ DefDef} ‘}’
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
---
2+
layout: doc-page
3+
title: "Translation of Extensions"
4+
---
5+
6+
Extensons are closely related to implicit classes and can be translated into them. In short,
7+
an extension that just adds extension methods translates into an implicit value class whereas an instance declaration translates into a regular implicit class. The following sections sketch this translation.
8+
9+
Conversely, it is conceivable (and desirable) to replace most usages of implicit classes and value classes by extensions and [opaque types](../opaques.html). We plan to [drop](../dropped/implicit-value-classes.html)
10+
these constructs in future versions of the language. Once that is achieved, the translations described
11+
below can be simply composed with the existing translations of implicit and value classes into the core language. It is
12+
not necessary to retain implicit and value classes as an intermediate step.
13+
14+
15+
### Translation of Extension Methods
16+
17+
Assume an extension
18+
19+
extension <id> <type-params> <implicit-params> for <type> { <defs> }
20+
21+
where both `<type-params>` and `<implicit-params>` can be absent.
22+
For simplicity assume that there are no context bounds on any of the type parameters
23+
in `<type-params>`. This is not an essential restriction as any such context bounds can be rewritten in a prior step to be evidence paramseters in `<implicit-params>`.
24+
25+
The extension is translated to the following implicit value class:
26+
27+
implicit class <id> <type-params> (private val $this: <type>) extends AnyVal {
28+
import $this._
29+
<defs'>
30+
}
31+
32+
Here `<defs'>` results from `<defs>` by augmenting any definition in <defs> with the parameters <implicit-params> and replacing any occurrence of `this` with `$this`.
33+
34+
For example, the extension
35+
36+
```scala
37+
extension SeqOps[T : math.Ordering] for Seq[T] {
38+
def indexOfLargest = this.zipWithIndex.maxBy(_._1)._2
39+
def indexOfSmallest = zipWithIndex.minBy(_._1)._2
40+
}
41+
```
42+
43+
would be translated to:
44+
45+
```scala
46+
implicit class SeqOps[T](private val $this: List[T]) extends AnyVal {
47+
import $this._
48+
def indexOfLargest (implicit $ev: math.Ordering[T]) = $this.zipWithIndex.maxBy(_._1)._2
49+
def indexOfSmallest(implicit $ev: math.Ordering[T]) = zipWithIndex.minBy(_._1)._2
50+
}
51+
```
52+
53+
### Translation of Instance Declarations
54+
55+
Now, assume an extension
56+
57+
extension if <type-params> <implicit-params> for <type> : <parents> { <body> }
58+
59+
where `<type-params>`, `<implicit-params>` and `<type>` are as before.
60+
This extension is translated to
61+
62+
implicit class <id> <type-params> ($this: <type>) <implicit-params> extends <parents> {
63+
import $this._
64+
<body'>
65+
}
66+
67+
As before, `<body'>` is computed from `<body>` by replacing any occurrence of `this` with `$this`. However, all parameters in <implicit-params> now stay on the class definition, instead of being distributed to all members in `<body>`. This is necessary in general, since `<body>` might contain value definitions or other statements that cannot be
68+
parameterized.
69+
70+
For example, the extension
71+
72+
```scala
73+
extension HasEqlImpl[T : Eql) for T : HasEql[T] {
74+
def === (that: T): Boolean = implicitly[Eql[T]].eql(this, that)
75+
}
76+
```
77+
78+
would be translated to
79+
80+
```scala
81+
implicit class HasEqlForEql[T]($this: T)(implicit $ev: Eql[T]) extends HasEql[T] {
82+
import $this._
83+
def === (that: T): Boolean = implicitly[Eql[T]].eql($this, that)
84+
}
85+
```

0 commit comments

Comments
 (0)