|
| 1 | +--- |
| 2 | +layout: blog |
| 3 | +title: Implicit Function Types |
| 4 | +author: Martin Odersky |
| 5 | +authorImg: /images/martin.jpg |
| 6 | +--- |
| 7 | + |
| 8 | +I just made the [first pull request](https://github.com/lampepfl/dotty/pull/1775) to add _implicit function types_ to |
| 9 | +Scala. I am pretty excited about it, because - citing the explanation |
| 10 | +of the pull request - "_This is the first step to bring contextual |
| 11 | +abstraction to Scala_". What do I mean by this? |
| 12 | + |
| 13 | +**Abstraction**: The ability to name a concept and use just the name afterwards. |
| 14 | + |
| 15 | +**Contextual**: A piece of a program produces results or outputs in |
| 16 | +some context. Our programming languages are very good in describing |
| 17 | +and abstracting what outputs are produced. But there's hardly anything |
| 18 | +yet available to abstract over the inputs that programs get from their |
| 19 | +context. Many interesting scenarios fall into that category, |
| 20 | +including: |
| 21 | + |
| 22 | + - passing configuration data to the parts of a system that need them, |
| 23 | + - managing capabilities for security critical tasks, |
| 24 | + - wiring components up with dependency injection, |
| 25 | + - defining the meanings of operations with type classes, |
| 26 | + - more generally, passing any sort of context to a computation. |
| 27 | + |
| 28 | +Implicit function types are a surprisingly simple and general way to |
| 29 | +make coding patterns solving these tasks abstractable, reducing |
| 30 | +boilerplate code and increasing applicability. |
| 31 | + |
| 32 | +**First Step**: My pull request is a first implementation. It solves the |
| 33 | + problem in principle, but introduces some run-time overhead. The |
| 34 | + next step will be to eliminate the run-time overhead through some |
| 35 | + simple optimizations. |
| 36 | + |
| 37 | +## Implicit Parameters |
| 38 | + |
| 39 | +In a functional setting, the inputs to a computation are most |
| 40 | +naturally expressed as _parameters_. One could simply augment |
| 41 | +functions to take additional parameters that represent configurations, |
| 42 | +capabilities, dictionaries, or whatever contextual data the functions |
| 43 | +need. The only downside with this is that often there's a large |
| 44 | +distance in the call graph between the definition of a contextual |
| 45 | +element and the site where it is used. Consequently, it becomes |
| 46 | +tedious to define all those intermediate parameters and to pass them |
| 47 | +along to where they are eventually consumed. |
| 48 | + |
| 49 | +Implicit parameters solve one half of the problem. Implicit |
| 50 | +parameters do not have to be propagated using boilerplate code; the |
| 51 | +compiler takes care of that. This makes them practical in many |
| 52 | +scenarios where plain parameters would be too cumbersome. For |
| 53 | +instance, type classes would be a lot less popular if one would have |
| 54 | +to pass all dictionaries by hand. Implicit parameters are also very |
| 55 | +useful as a general context passing mechanism. For instance in the |
| 56 | +_dotty_ compiler, almost every function takes an implicit context |
| 57 | +parameter which defines all elements relating to the current state of |
| 58 | +the compilation. This is in my experience much better than the cake |
| 59 | +pattern because it is lightweight and can express context changes in a |
| 60 | +purely functional way. |
| 61 | + |
| 62 | +The main downside of implicit parameters is the verbosity of their |
| 63 | +declaration syntax. It's hard to illustrate this with a smallish example, |
| 64 | +because it really only becomes a problem at scale, but let's try anyway. |
| 65 | + |
| 66 | +Let's say we want to write some piece of code that's designed to run |
| 67 | +in a transaction. For the sake of illustration here's a simple transaction class: |
| 68 | + |
| 69 | + class Transaction { |
| 70 | + private val log = new ListBuffer[String] |
| 71 | + def println(s: String): Unit = log += s |
| 72 | + |
| 73 | + private var aborted = false |
| 74 | + private var committed = false |
| 75 | + |
| 76 | + def abort(): Unit = { aborted = true } |
| 77 | + def isAborted = aborted |
| 78 | + |
| 79 | + def commit(): Unit = |
| 80 | + if (!aborted && !committed) { |
| 81 | + Console.println("******* log ********") |
| 82 | + log.foreach(Console.println) |
| 83 | + committed = true |
| 84 | + } |
| 85 | + } |
| 86 | + |
| 87 | +The transaction encapsulates a log, to which one can print messages. |
| 88 | +It can be in one of three states: running, committed, or aborted. |
| 89 | +If the transaction is committed, it prints the stored log to the console. |
| 90 | + |
| 91 | +The `transaction` method lets one run some given code `op` inside |
| 92 | +a newly created transaction: |
| 93 | + |
| 94 | + def transaction[T](op: Transaction => T) = { |
| 95 | + val trans: Transaction = new Transaction |
| 96 | + op(trans) |
| 97 | + trans.commit() |
| 98 | + } |
| 99 | + |
| 100 | +The current transaction needs to be passed along a call chain to all |
| 101 | +the places that need to access it. To illustrate this, here are three |
| 102 | +functions `f1`, `f2` and `f3` which call each other, and also access |
| 103 | +the current transaction. The most convenient way to achieve this is |
| 104 | +by passing the current transaction as an implicit parameter. |
| 105 | + |
| 106 | + def f1(x: Int)(implicit thisTransaction: Transaction): Int = { |
| 107 | + thisTransaction.println(s"first step: $x") |
| 108 | + f2(x + 1) |
| 109 | + } |
| 110 | + def f2(x: Int)(implicit thisTransaction: Transaction): Int = { |
| 111 | + thisTransaction.println(s"second step: $x") |
| 112 | + f3(x * x) |
| 113 | + } |
| 114 | + def f3(x: Int)(implicit thisTransaction: Transaction): Int = { |
| 115 | + thisTransaction.println(s"third step: $x") |
| 116 | + if (x % 2 != 0) thisTransaction.abort() |
| 117 | + x |
| 118 | + } |
| 119 | + |
| 120 | +The main program calls `f1` in a fresh transaction context and prints |
| 121 | +its result: |
| 122 | + |
| 123 | + def main(args: Array[String]) = { |
| 124 | + transaction { |
| 125 | + implicit thisTransaction => |
| 126 | + val res = f1(args.length) |
| 127 | + println(if (thisTransaction.isAborted) "aborted" else s"result: $res") |
| 128 | + } |
| 129 | + } |
| 130 | + |
| 131 | +Two sample calls of the program (let's call it `TransactionDemo`) are here: |
| 132 | + |
| 133 | + scala TransactionDemo 1 2 3 |
| 134 | + result: 16 |
| 135 | + ******* log ******** |
| 136 | + first step: 3 |
| 137 | + second step: 4 |
| 138 | + third step: 16 |
| 139 | + |
| 140 | + scala TransactionDemo 1 2 3 4 |
| 141 | + aborted |
| 142 | + |
| 143 | +So far, so good. The code above is quite compact as far as expressions |
| 144 | +are concerned. In particular, it's nice that, being implicit |
| 145 | +parameters, none of the transaction values had to be passed along |
| 146 | +explicitly in a call. But on the definition side, things are less |
| 147 | +rosy: Every one of the functions `f1` to `f3` needed an additional |
| 148 | +implicit parameter: |
| 149 | + |
| 150 | + (implicit thisTransaction: Transaction) |
| 151 | + |
| 152 | +Having to repeat three-times might not look so bad here, but it certainly |
| 153 | +smells of boilerplate. In real-sized projects, this can get much worse. |
| 154 | +For instance, the _dotty_ compiler uses implicit abstraction |
| 155 | +over contexts for most of its parts. Consequently it ends up with currently |
| 156 | +no fewer than 2641 occurrences of the text string |
| 157 | + |
| 158 | + (implicit ctx: Context) |
| 159 | + |
| 160 | +It would be nice if we could get rid of them. |
| 161 | + |
| 162 | +## Implicit Functions |
| 163 | + |
| 164 | +Let's massage the definition of `f1` a bit by moving the last parameter section to the right of the equals sign: |
| 165 | + |
| 166 | + def f1(x: Int) = { implicit thisTransaction: Transaction => |
| 167 | + thisTransaction.println(s"first step: $x") |
| 168 | + f2(x + 1) |
| 169 | + } |
| 170 | + |
| 171 | +The right hand side of this new version of `f1` is now an implicit |
| 172 | +function value. What's the type of this value? Previously, it was |
| 173 | +`Transaction => Int`, that is, the knowledge that the function has an |
| 174 | +implicit parameter got lost in the type. The main extension implemented by |
| 175 | +the pull request is to introduce implicit function types that mirror |
| 176 | +the implicit function values which we have already. Concretely, the new |
| 177 | +type of `f1` is: |
| 178 | + |
| 179 | + implicit Transaction => Int |
| 180 | + |
| 181 | +Just like the normal function type syntax `A => B`, desugars to `scala.Function1[A, B]` |
| 182 | +the implicit function type syntax `implicit A => B` desugars to `scala.ImplicitFunction1[A, B]`. |
| 183 | +The same holds at other function arities. With dotty's [pull request #1758](https://github.com/lampepfl/dotty/pull/1758) |
| 184 | +merged there is no longer an upper limit of 22 for such functions. |
| 185 | + |
| 186 | +The type `ImplicitFunction1` can be thought of being defined as follows: |
| 187 | + |
| 188 | + trait ImplicitFunction1[-T0, R] extends Function1[T0, R] { |
| 189 | + override def apply(implicit x: T0): R |
| 190 | + } |
| 191 | + |
| 192 | +However, you won't find a classfile for this trait because all implicit function traits |
| 193 | +get mapped to normal functions during type erasure. |
| 194 | + |
| 195 | +There are two rules that guide type checking of implicit function types. |
| 196 | +The first rule says that an implicit function is applied to implicit arguments |
| 197 | +in the same way an implicit method is. More precisely, if `t` is an expression |
| 198 | +of an implicit function type |
| 199 | + |
| 200 | + t: implicit (T1, ..., Tn) => R |
| 201 | + |
| 202 | +such that `t` is not an implicit closure itself and `t` is not the |
| 203 | +prefix of a call `t.apply(...)`, then an `apply` is implicitly |
| 204 | +inserted, so `t` becomes `t.apply`. We have already seen that the |
| 205 | +definition of `t.apply` is an implicit method as given in the |
| 206 | +corresponding implicit function trait. Hence, it will in turn be |
| 207 | +applied to a matching sequence of implicit arguments. The end effect is |
| 208 | +that references to implicit functions get applied to implicit arguments in the |
| 209 | +same way as references to implicit methods. |
| 210 | + |
| 211 | +The second rule is the dual of the first. If the expected type |
| 212 | +of an expression `t` is an implicit function type |
| 213 | + |
| 214 | + implicit (T1, ..., Tn) => R |
| 215 | + |
| 216 | +then `t` is converted to an implicit closure, unless it is already one. |
| 217 | +More precisely, `t` is mapped to the implicit closure |
| 218 | + |
| 219 | + implicit ($ev1: T1, ..., $evn: Tn) => t |
| 220 | + |
| 221 | +The parameter names of this closure are compiler-generated identifiers |
| 222 | +which should not be accessed from user code. That is, the only way to |
| 223 | +refer to an implicit parameter of a compiler-generated function is via |
| 224 | +`implicitly`. |
| 225 | + |
| 226 | +It is important to note that this second conversion needs to be applied |
| 227 | +_before_ the expression `t` is typechecked. This is because the |
| 228 | +conversion establishes the necessary context to make type checking `t` |
| 229 | +succeed by defining the required implicit parameters. |
| 230 | + |
| 231 | +There is one final tweak to make this all work: When using implicit parameters |
| 232 | +for nested functions it was so far necessary to give all implicit parameters |
| 233 | +of the same type the same name, or else one would get ambiguities. For instance, consider the |
| 234 | +following fragment: |
| 235 | + |
| 236 | + def f(implicit c: C) = { |
| 237 | + def g(implicit c: C) = ... implicitly[C] ... |
| 238 | + ... |
| 239 | + } |
| 240 | + |
| 241 | +If we had named the inner parameter `d` instead of `c` we would |
| 242 | +have gotten an implicit ambiguity at the call of `implicitly` because |
| 243 | +both `c` and `d` would be eligible: |
| 244 | + |
| 245 | + def f(implicit c: C) = { |
| 246 | + def g(implicit d: C) = ... implicitly[C] ... // error! |
| 247 | + ... |
| 248 | + } |
| 249 | + |
| 250 | +The problem is that parameters in implicit closures now have |
| 251 | +compiler-generated names, so the programmer cannot enforce the proper |
| 252 | +naming scheme to avoid all ambiguities. We fix the problem by |
| 253 | +introducing a new disambiguation rule which makes nested occurrences |
| 254 | +of an implicit take precedence over outer ones. This rule, which |
| 255 | +applies to all implicit parameters and implicit locals, is conceptually |
| 256 | +analogous to the rule that prefers implicits defined in companion |
| 257 | +objects of subclasses over those defined in companion objects of |
| 258 | +superclass. With that new disambiguation rule the example code above |
| 259 | +now compiles. |
| 260 | + |
| 261 | +That's the complete set of rules needed to deal with implicit function types. |
| 262 | + |
| 263 | +## How to Remove Boilerplate |
| 264 | + |
| 265 | +The main advantage of implicit function types is that, being types, |
| 266 | +they can be abstracted. That is, one can define a name for an implicit |
| 267 | +function type and then use just the name instead of the full type. |
| 268 | +Let's revisit our previous example and see how it can be made more |
| 269 | +concise using this technique. |
| 270 | + |
| 271 | +We first define a type `Transactional` for functions that take an implicit parameter of type `Transaction`: |
| 272 | + |
| 273 | + type Transactional[T] = implicit Transaction => T |
| 274 | + |
| 275 | +Making the return type of `f1` to `f3` a `Transactional[Int]`, we can |
| 276 | +eliminate their implicit parameter sections: |
| 277 | + |
| 278 | + def f1(x: Int): Transactional[Int] = { |
| 279 | + thisTransaction.println(s"first step: $x") |
| 280 | + f2(x + 1) |
| 281 | + } |
| 282 | + def f2(x: Int): Transactional[Int] = { |
| 283 | + thisTransaction.println(s"second step: $x") |
| 284 | + f3(x * x) |
| 285 | + } |
| 286 | + def f3(x: Int): Transactional[Int] = { |
| 287 | + thisTransaction.println(s"third step: $x") |
| 288 | + if (x % 2 != 0) thisTransaction.abort() |
| 289 | + x |
| 290 | + } |
| 291 | + |
| 292 | +You might ask, how does `thisTransaction` typecheck, since there is no |
| 293 | +longer a parameter with that name? In fact, `thisTransaction` is now a |
| 294 | +global definition: |
| 295 | + |
| 296 | + def thisTransaction: Transactional[Transaction] = implicitly[Transaction] |
| 297 | + |
| 298 | +You might ask: a `Transactional[Transaction]`, is that not circular? To see more clearly, let's expand |
| 299 | +the definition according to the rules given in the last section. `thisTransaction` |
| 300 | +is of implicit function type, so the right hand side is expanded to the |
| 301 | +implicit closure |
| 302 | + |
| 303 | + implicit ($ev0: Transaction) => implicitly[Transaction] |
| 304 | + |
| 305 | +The right hand side of this closure, `implicitly[Transaction]`, needs |
| 306 | +an implicit parameter of type `Transaction`, so the closure is further |
| 307 | +expanded to |
| 308 | + |
| 309 | + implicit ($ev0: Transaction) => implicitly[Transaction]($ev0) |
| 310 | + |
| 311 | +Now, `implicitly` is defined in `scala.Predef` like this: |
| 312 | + |
| 313 | + def implicitly[T](implicit x: T) = x |
| 314 | + |
| 315 | +If we plug that definition into the closure above and simplify, we get: |
| 316 | + |
| 317 | + implicit ($ev0: Transaction) => $ev0 |
| 318 | + |
| 319 | +So, `thisTransaction` is just the implicit identity function on `transaction`! |
| 320 | +In other words, if we use `thisTransaction` in the body of `f1` to `f3`, it will |
| 321 | +pick up and return the unnamed implicit parameter that's in scope. |
| 322 | + |
| 323 | +Finally, here are the `transaction` and `main` method that complete |
| 324 | +the example. Since `transactional`'s parameter `op` is now a |
| 325 | +`Transactional`, we can eliminate the `Transaction` argument to `op` |
| 326 | +and the `Transaction` lambda in `main`; both will be added by the compiler. |
| 327 | + |
| 328 | + def transaction[T](op: Transactional[T]) = { |
| 329 | + implicit val trans: Transaction = new Transaction |
| 330 | + op |
| 331 | + trans.commit() |
| 332 | + } |
| 333 | + def main(args: Array[String]) = { |
| 334 | + transaction { |
| 335 | + val res = f1(args.length) |
| 336 | + println(if (thisTransaction.isAborted) "aborted" else s"result: $res") |
| 337 | + } |
| 338 | + } |
| 339 | + |
| 340 | +## Categorically Speaking |
| 341 | + |
| 342 | +There are many interesting connections with category theory to explore |
| 343 | +here. On the one hand, implicit functions are used for tasks that are |
| 344 | +sometimes covered with monads such as the reader monad. There's an |
| 345 | +argument to be made that implicits have better composability than |
| 346 | +monads and why that is. |
| 347 | + |
| 348 | +On the other hand, it turns out that implicit functions can also be |
| 349 | +given a co-monadic interpretation, and the interplay between monads and |
| 350 | +comonads is very interesting in its own right. |
| 351 | + |
| 352 | +But these discussions will have to wait for another time, as |
| 353 | +this blog post is already too long. |
| 354 | + |
| 355 | +## Conclusion |
| 356 | + |
| 357 | +Implicit function types are a unique way to abstract over the context in |
| 358 | +which some piece of code is run. I believe they will deeply influence |
| 359 | +the way we write Scala in the future. They are very powerful |
| 360 | +abstractions, in the sense that just declaring a type of a function |
| 361 | +will inject certain implicit values into the scope of the function's |
| 362 | +implementation. Can this be abused, making code more obscure? |
| 363 | +Absolutely, like every other powerful abstraction technique. To keep |
| 364 | +your code sane, please keep the [Principle of Least Power](http://www.lihaoyi.com/post/StrategicScalaStylePrincipleofLeastPower.html) in mind. |
0 commit comments