|
| 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 | + } |
0 commit comments