Skip to content

Commit 24a1890

Browse files
committed
Add docs for extension methods
1 parent bfab87b commit 24a1890

File tree

5 files changed

+251
-0
lines changed

5 files changed

+251
-0
lines changed
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
---
2+
layout: doc-page
3+
title: "Method Augmentations"
4+
---
5+
6+
Method augmentations are a way to define _extension methods_. Here is a simple one:
7+
8+
```scala
9+
case class Circle(x: Double, y: Double, radius: Double)
10+
11+
augment Circle {
12+
def circumference: Double = this.radius * math.Pi * 2
13+
}
14+
```
15+
16+
The `augment Circle` clause adds an extension 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`. Unlike for regular methods,
28+
an explicit `this` is mandatory to refer to members of the receiver. So the following
29+
gives a compilation error:
30+
31+
```scala
32+
| def circumference = radius * math.Pi * 2
33+
| ^^^^^^
34+
| not found: radius
35+
```
36+
37+
### Scope of Augment Clauses
38+
39+
Augment clauses can appear anywhere in a program; there is no need to co-define them with the types they augment. Extension methods are available whereever their defining augment clause is in scope. Augment clauses can be inherited or wildcard-imported like normal definitions. This is usually sufficient to control their visibility. If more control is desired, one can also attach a name to an augment clause, like this:
40+
41+
```scala
42+
package shapeOps
43+
44+
augment circleOps @ Circle {
45+
def circumference: Double = this.radius * math.Pi * 2
46+
def area: Double = this.radius * this.radius * math.Pi
47+
}
48+
```
49+
Labelled augments can be imported individually by their name:
50+
51+
```scala
52+
import shapeOps.circleOps // makes circumference and area available
53+
```
54+
55+
### Augmented Types
56+
57+
An augment clause may add methods to arbitrary types. For instance, the following
58+
clause adds a `longestStrings` extension method to a `Seq[String]`:
59+
60+
```scala
61+
augment Seq[String] {
62+
def longestStrings: Seq[String] = {
63+
val maxLength = this.map(_.length).max
64+
this.filter(_.length == maxLength)
65+
}
66+
}
67+
```
68+
69+
### Augmented Type Patterns
70+
71+
The previous example augmented a specific instance of a generic type. It is also possible
72+
to augment a generic type itself, using a _type pattern_:
73+
74+
```scala
75+
augment List[type T] {
76+
def second: T = this.tail.head
77+
}
78+
```
79+
80+
The `type T` argument indicates that the augment applies to `List[T]`s for any type `T`.
81+
We also say that `type T` introduces `T` as a variable in the type pattern `List[type T]`.
82+
Type variables may appear anywhere in a type pattern. Example:
83+
84+
```scala
85+
augment List[List[type T]] {
86+
def flattened: List[T] = this.foldLeft[List[T]](Nil)(_ ++ _)
87+
}
88+
```
89+
90+
### Type Patterns in Cases
91+
92+
The `type ...` syntax for pattern bound type variables also applies to patterns in
93+
case clauses of `match` and `try` expressions. For instance:
94+
95+
```scala
96+
def f[T](s: Set[T], x: T) = s match {
97+
case _: SortedSet[type U] => ... // binds `U`, infers that `U = T`
98+
case _ =>
99+
}
100+
```
101+
102+
Previously, one used a lower-case name to indicate a variable in a type pattern, as in:
103+
104+
```scala
105+
case _: SortedSet[u] => ... // binds `u`, infers that `u = T`
106+
```
107+
108+
While being more regular wrt term variables in patterns, this usage is harder to read, and has the problem that it feels unnatrual to have to write type names in lower case. It will therefore be phased out to be replaced by the explicit `type T` syntax.
109+
110+
Type patterns in cases only come in unbounded form; the bounds defined in the next section are not applicable to them.
111+
112+
### Bounds in Augmented Type Patterns
113+
114+
It is also possible to use bounds for the type variables in an augmented type pattern. Examples:
115+
116+
```scala
117+
augment List[type T <: Shape] {
118+
def totalArea = this.map(_.area).sum
119+
}
120+
```
121+
122+
Context-bounds are also supported:
123+
124+
```scala
125+
augment Seq[type T: math.Ordering] {
126+
def indexOfLargest = this.zipWithIndex.maxBy(_._1)._2
127+
def indexOfSmallest = this.zipWithIndex.minBy(_._1)._2
128+
}
129+
```
130+
131+
### Implicit Parameters for Type Patterns
132+
133+
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:
134+
135+
```scala
136+
augment Seq[type T](implicit ev: math.Ordering[T]) {
137+
def indexOfLargest = this.zipWithIndex.maxBy(_._1)._2
138+
def indexOfSmallest = this.zipWithIndex.minBy(_._1)._2
139+
}
140+
```
141+
142+
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.
143+
144+
### Toplevel Type Variables
145+
146+
A type pattern consisting of a top-level typevariable introduces a fully generic augmentation. For instance, the following augment introduces `x ~ y` as an alias
147+
for `(x, y)`:
148+
149+
```scala
150+
augment (type T) {
151+
def ~ [U](that: U) = (this, that)
152+
}
153+
```
154+
155+
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 augments to provide a zero-overhead abstraction.
156+
157+
```scala
158+
object PostConditions {
159+
opaque type WrappedResult[T] = T
160+
161+
private object WrappedResult {
162+
def wrap[T](x: T): WrappedResult[T] = x
163+
def unwrap[T](x: WrappedResult[T]): T = x
164+
}
165+
166+
def result[T](implicit er: WrappedResult[T]): T = WrappedResult.unwrap(er)
167+
168+
augment (type T) {
169+
def ensuring[U](condition: implicit WrappedResult[T] => Boolean): T = {
170+
implicit val wrapped = WrappedResult.wrap(this)
171+
assert(condition)
172+
this
173+
}
174+
}
175+
}
176+
object Test {
177+
import PostConditions._
178+
val s = List(1, 2, 3).sum.ensuring(result == 6)
179+
}
180+
```
181+
**Explanations**: We use an implicit function type `implicit WrappedResult[T] => Boolean`
182+
as the type of the condition of `ensuring`. An argument condition to `ensuring` such as
183+
`(result == 6)` will therefore have an implicit value of type `WrappedResult[T]` in scope
184+
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:
185+
186+
{ val result = List(1, 2, 3).sum
187+
assert(result == 6)
188+
result
189+
}

docs/sidebar.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,16 @@ sidebar:
3131
url: docs/reference/phantom-types.html
3232
- title: Literal Singleton Types
3333
url: docs/reference/singleton-types.html
34+
- title: Augmentations
35+
subsection:
36+
- title: Method Augmentations
37+
url: docs/reference/augments/method-augments.html
38+
- title: Type Patterns
39+
url: docs/reference/augments/type-patterns.html
40+
- title: Trait Augmentations
41+
url: docs/reference/augments/trait-augments.html
42+
- title: Translation of Augmentations
43+
url: docs/reference/augments/translation.html
3444
- title: Enums
3545
subsection:
3646
- title: Enumerations

tests/neg/augment.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ object augments {
1010
private val p = math.Pi // error: `def` expected
1111
}
1212

13+
augment Circle {
14+
def circumference = radius * math.Pi * 2 // error: not found
15+
}
16+
1317
// Trait implementations
1418

1519
trait HasArea {

tests/pos/typepats.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,12 @@ object Test {
1010
case _: C[type T] =>
1111
val x: T = 3
1212
}
13+
14+
import collection.immutable.SortedSet
15+
val s: Set[Int] = SortedSet(1, 2, 3)
16+
s match {
17+
case _: SortedSet[type T] =>
18+
val x: T = 3
19+
case _ =>
20+
}
1321
}

tests/run/augment.scala

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,46 @@ object augments2 {
9494

9595
}
9696

97+
object docs {
98+
augment Seq[String] {
99+
def longestStrings: Seq[String] = {
100+
val maxLength = this.map(_.length).max
101+
this.filter(_.length == maxLength)
102+
}
103+
}
104+
105+
augment List[List[type T]] {
106+
def flattened: List[T] = this.foldLeft[List[T]](Nil)(_ ++ _)
107+
}
108+
109+
augment Seq[type T: math.Ordering] {
110+
def indexOfLargest = this.zipWithIndex.maxBy(_._1)._2
111+
def indexOfSmallest = this.zipWithIndex.minBy(_._1)._2
112+
}
113+
114+
object PostConditions {
115+
opaque type EnsureResult[T] = T
116+
117+
private object EnsureResult {
118+
def wrap[T](x: T): EnsureResult[T] = x
119+
def unwrap[T](x: EnsureResult[T]): T = x
120+
}
121+
122+
def result[T](implicit er: EnsureResult[T]): T = EnsureResult.unwrap(er)
123+
124+
augment (type T) {
125+
def ensuring[U](f: implicit EnsureResult[T] => Boolean): T = {
126+
assert(f(EnsureResult.wrap(this)))
127+
this
128+
}
129+
}
130+
}
131+
import PostConditions._
132+
133+
val s = List(1, 2, 3).sum.ensuring(result == 6)
134+
135+
}
136+
97137
import augments._
98138
import augments2.{flatLists, samePairs}
99139
object Test extends App {

0 commit comments

Comments
 (0)