Skip to content

Commit e95bb6f

Browse files
committed
New proposal for extension methods
1 parent 72eabc0 commit e95bb6f

File tree

2 files changed

+255
-3
lines changed

2 files changed

+255
-3
lines changed

docs/docs/internals/syntax.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -316,10 +316,8 @@ ValDcl ::= ids ‘:’ Type
316316
VarDcl ::= ids ‘:’ Type PatDef(_, ids, tpe, EmptyTree)
317317
DefDcl ::= DefSig [‘:’ Type] DefDef(_, name, tparams, vparamss, tpe, EmptyTree)
318318
DefSig ::= id [DefTypeParamClause] DefParamClauses
319-
| DefParamClause ‘.’
319+
| DefParamClause [‘.’]
320320
id [DefTypeParamClause] DefParamClauses
321-
| DefParamClause
322-
id [DefTypeParamClause] DefParamClause DefParamClauses
323321
324322
TypeDcl ::= id [TypTypeParamClause] [‘=’ Type] TypeDefTree(_, name, tparams, tpt)
325323
| id [HkTypeParamClause] TypeBounds TypeDefTree(_, name, tparams, bounds)
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
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+
implicit object CircleOps {
12+
def (c: Circle).circumference: Double = c.radius * math.Pi * 2
13+
}
14+
```
15+
16+
`CircleOps` 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+
### Translation of Extension Methods
24+
25+
Extension methods are methods that have a parameter clause in front of the defined
26+
identifier. They translate to methods where the leading parameter section is moved
27+
to after the defined identifier. So, the definition of `circumference` above translates
28+
to the plain method, and can also be invoked as such:
29+
```scala
30+
def circumference(c: Circle): Double = c.radius * math.Pi * 2
31+
32+
assert(circle.circumference == CircleOps.circumference(circle))
33+
```
34+
35+
### Translation of Calls to Extension Methods
36+
37+
The rules for resolving a selection `e.m` are augmented as follows: If `m` is not a
38+
member of the type `T` of `e`, and there is an implicit value `i` that defines `m`
39+
in either the current scope or in the implicit scope of `T`, then `e.m` is expanded
40+
to `i.m(e)`. This expansion is attempted at the time where the compiler also tries an implicit conversion from `T` to a type containing `m`. If there is more than one way
41+
of expanding, an ambiguity error results.
42+
43+
So `circle.circumference` translates to `CircleOps.circumference(circle)`, provided
44+
`circle` has type `Circle` and `CircleOps` is an eligible implicit (i.e. it is visible at the point of call or it is defined in the companion object of `Circle`).
45+
46+
### Extended Types
47+
48+
Extension methods can be added to arbitrary types. For instance, the following
49+
object adds a `longestStrings` extension method to a `Seq[String]`:
50+
51+
```scala
52+
implicit object StringOps {
53+
def (xs: Seq[String).longestStrings = {
54+
val maxLength = xs.map(_.length).max
55+
xs.filter(_.length == maxLength)
56+
}
57+
}
58+
```
59+
60+
### Generic Extensions
61+
62+
The previous example extended a specific instance of a generic type. It is also possible
63+
to extend a generic type by adding type parameters to an extension method:
64+
65+
```scala
66+
implicit object ListOps {
67+
def (xs: List[T]).second[T] = xs.tail.head
68+
}
69+
```
70+
71+
or:
72+
73+
74+
```scala
75+
implicit object ListListOps {
76+
def (xs: List[List[T]]).flattened[T] = xs.foldLeft[List[T]](Nil)(_ ++ _)
77+
}
78+
```
79+
80+
As usual, type parameters of the extension method follow the defined method name. Nevertheless, such type parameters can already be used in the parameter clause that precedes
81+
the defined method name.
82+
83+
### A Larger Example
84+
85+
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.
86+
87+
```scala
88+
object PostConditions {
89+
opaque type WrappedResult[T] = T
90+
91+
private object WrappedResult {
92+
def wrap[T](x: T): WrappedResult[T] = x
93+
def unwrap[T](x: WrappedResult[T]): T = x
94+
}
95+
96+
def result[T](implicit er: WrappedResult[T]): T = WrappedResult.unwrap(er)
97+
98+
implicit object Ensuring {
99+
def (x: T).ensuring(condition: implicit WrappedResult[T] => Boolean): T = {
100+
implicit val wrapped = WrappedResult.wrap(this)
101+
assert(condition)
102+
this
103+
}
104+
}
105+
}
106+
107+
object Test {
108+
import PostConditions._
109+
val s = List(1, 2, 3).sum.ensuring(result == 6)
110+
}
111+
```
112+
**Explanations**: We use an implicit function type `implicit WrappedResult[T] => Boolean`
113+
as the type of the condition of `ensuring`. An argument condition to `ensuring` such as
114+
`(result == 6)` will therefore have an implicit value of type `WrappedResult[T]` in scope
115+
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:
116+
117+
{ val result = List(1, 2, 3).sum
118+
assert(result == 6)
119+
result
120+
}
121+
122+
### Extension Operators
123+
124+
The `.` between leading parameter section and defined name in an extension method is optional. If the extension method is an operator, leaving out the dot leads to clearer
125+
syntax that resembles the intended usage pattern:
126+
127+
```scala
128+
implicit object NumericOps {
129+
def (x: T) + [T : Numeric](y: T): T = implicitly[Numeric[T]].plus(x, y)
130+
}
131+
```
132+
133+
An infix operation `x op y` of an extension method `op` coming from `z` is always translated to `z.op(x)(y)`, irrespective of whether `op` is right-associative or not. So, no implicit swapping of arguments takes place for extension methods ending in a `:`. For instance,
134+
here is the "domino"-operator brought back as an extension method:
135+
136+
```scala
137+
implicit object SeqOps {
138+
def (x: A) /: [A, B](xs: Seq[B])(op: (A, B) => A): A =
139+
xs.foldLeft(x)(op)
140+
}
141+
```
142+
A call like
143+
```scala
144+
(0 /: List(1, 2, 3)) (_ + _)
145+
```
146+
is translated to
147+
```scala
148+
SeqOps./: (0) (List(1, 2, 3)) (_ + _)
149+
```
150+
151+
### Extension Methods and TypeClasses
152+
153+
The rules for expanding extension methods make sure that they work seamlessly with typeclasses. For instance, consider `SemiGroup` and `Monoid`.
154+
```scala
155+
// Two typeclasses:
156+
trait SemiGroup[T] {
157+
def (x: T).combine(y: T): T
158+
}
159+
trait Monoid[T] extends SemiGroup[T] {
160+
def unit: T
161+
}
162+
163+
// An instance declaration:
164+
implicit object StringMonoid extends Monoid[String] {
165+
def (x: String).combine(y: String): String
166+
def unit: String
167+
}
168+
169+
// Abstracting over a typeclass with a context bound:
170+
def sum[T: Monoid](xs: List[T]): T =
171+
xs.foldLeft(implicitly[Monoid[T]].unit)(_.combine(_))
172+
```
173+
In the last line, the call to `_combine(_)` expands to `(x1, x2) => x1.combine(x)`,
174+
which expands in turn to `(x1, x2) => ev.combine(x1, x2)` where `ev` is the implicit
175+
evidence parameter summoned by the context bound `[T: Monoid]`. This works since
176+
extension methods apply everywhere their enclosing object is available as an implicit.
177+
178+
### Generic Extension Classes
179+
180+
As another example, consider implementations of an `Ord` type class:
181+
```scala
182+
trait Ord[T]
183+
def (x: T).compareTo(y: T): Int
184+
def (x: T) < (that: T) = x.compareTo(y) < 0
185+
def (x: T) > (that: T) = x.compareTo(y) > 0
186+
}
187+
188+
implicit object IntOrd {
189+
def (x: Int).compareTo(y: Int) =
190+
if (x < y) -1 else if (x > y) +1 else 0
191+
}
192+
193+
implicit class ListOrd[T: Ord] {
194+
def (xs: List[T]).compareTo(ys: List[T]): Int = (xs, ys) match
195+
case (Nil, Nil) => 0
196+
case (Nil, _) => -1
197+
case (_, Nil) => +1
198+
case (x :: xs1, y :: ys1) =>
199+
val fst = x.compareTo(y)
200+
if (fst != 0) fst else xs1.compareTo(ys1)
201+
}
202+
203+
def max[T: Ord](x: T, y: T) = if (x < y) y else x
204+
```
205+
The `ListOrd` class is generic - it works for any type argument `T` that is itself an instance of `Ord`. In current Scala, we could not define `ListOrd` as an implicit class since
206+
implicit classes can only define implicit converions that take exactly one non-implicit
207+
value parameter. We propose to drop this requirement and to also allow implicit classes
208+
without any value parameters, or with only implicit value parameters. The generated implicit method would in each case follow the signature of the class. That is, for `ListOrd` we'd generate the method:
209+
```scala
210+
implicit def ListOrd[T: Ord]: ListOrd[T] = new ListOrd[T]
211+
```
212+
213+
### Higher Kinds
214+
215+
Extension methods generalize to higher-kinded types without requiring special provisions. Example:
216+
217+
```scala
218+
trait Functor[F[_]] {
219+
def (x: F[A]).map[A, B](f: A => B): F[B]
220+
}
221+
222+
trait Monad[F[_]] extends Functor[F] {
223+
def (x: F[A]).flatMap[A, B](f: A => F[B]): F[B]
224+
def (x: F[A]).map[A, B](f: A => B) = x.flatMap(f `andThen` pure)
225+
226+
def pure[A]: F[A]
227+
}
228+
229+
implicit object ListMonad extends Monad[List] {
230+
def (xs: List[A]).flatMap[A, B](f: A => List[B]): List[B] =
231+
xs.flatMap(f)
232+
def pure[A]: List[A] =
233+
List.Nil
234+
}
235+
236+
implicit class ReaderMonad[Ctx] extends Monad[[X] => Ctx => X] {
237+
def (r: Ctx => A).flatMap[A, B](f: A => Ctx => B): Ctx => B =
238+
ctx => f(r(ctx))(ctx)
239+
def pure[A](x: A): Ctx =>
240+
A = ctx => x
241+
}
242+
```
243+
### Syntax
244+
245+
The required syntax extension just adds one clause for extension methods relative
246+
to the [current syntax](https://github.com/lampepfl/dotty/blob/master/docs/docs/internals/syntax.md).
247+
```
248+
DefSig ::= ...
249+
| DefParamClause [‘.’] id [DefTypeParamClause] DefParamClauses
250+
```
251+
252+
253+
254+

0 commit comments

Comments
 (0)