Skip to content

Commit 68525f5

Browse files
authored
Merge pull request #570 from odersky/blog-implicit-functions
Blog post on implicit functions
2 parents 3fe481b + 1007577 commit 68525f5

File tree

1 file changed

+364
-0
lines changed

1 file changed

+364
-0
lines changed
Lines changed: 364 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,364 @@
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

Comments
 (0)