|
| 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