Skip to content

Commit c1cd7e1

Browse files
committed
Finished blog post
1 parent 30c7b73 commit c1cd7e1

File tree

1 file changed

+225
-35
lines changed

1 file changed

+225
-35
lines changed

docs/blog/_posts/2016-12-05-implicit-function-types.md

Lines changed: 225 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,58 +7,39 @@ authorImg: /images/martin.jpg
77

88
I just made the first pull request to add _implicit function types_ to
99
Scala. I am pretty excited about it, because, citing the explanation
10-
of the pull request "This is the first step to bring comonadic
11-
abstraction to Scala". That's quite a mouthful, so I better explain what I
10+
of the pull request "_This is the first step to bring contextual
11+
abstraction to Scala_". That's quite a mouthful, so I better explain what I
1212
mean by it.
1313

1414
Let me try to explain the words in this sentence from right to left.
1515

16-
*Scala*: I assume everyone who reads this understands that we mean the
16+
**Scala**: I assume everyone who reads this understands that we mean the
1717
programming language, not the opera house.
1818

19-
*Abstraction*: The ability to name a concept and use just the name afterwards.
19+
**Abstraction**: The ability to name a concept and use just the name afterwards.
2020

21-
*Comonadic*: In category theory, a _comonad_ is the dual of a
22-
_monad_. Roughly speaking, a monad is a way to wrap the result (or:
23-
outputs) of a computation in some other type. For instance
24-
`Future[T]` means that the result of type `T` will be produced at
25-
some later time on demand, or `Option[T]` indicates that the result
26-
might also be undefined.
27-
28-
Dually, a _comonad_ allows to transform or
29-
enrich or otherwise manipulate the _inputs_ to a computation.
30-
The inputs are typically what a computation can access in its
31-
environment. Interesting tasks that are by nature comonadic are
21+
**Contextual**: A piece of a program produces results or outputs in
22+
some context. Our programming languages are very good in describing
23+
and abstracting what outputs are produced. But there's hardly anything
24+
yet available to abstract over the inputs that programs get from their
25+
context. Many interesting scenarios fall into that category,
26+
including:
3227

3328
- passing configuration data to the parts of a system that need them,
3429
- managing capabilities for security critical tasks,
3530
- wiring components up with dependency injection,
3631
- defining the meanings of operations with type classes,
3732
- more generally, passing any sort of context to a computation.
3833

39-
Implicit function types are a suprisingly simple and general way to
34+
Implicit function types are a surprisingly simple and general way to
4035
make coding patterns solving these tasks abstractable, reducing
4136
boilerplate code and increasing applicability.
4237

43-
*First Step* My pull request is first implementation. In solves the
44-
problem in principle, but it introduces some run-time overhead. The
38+
**First Step**: My pull request is a first implementation. It solves the
39+
problem in principle, but introduces some run-time overhead. The
4540
next step will be to eliminate the run-time overhead through some
4641
simple optimizations.
4742

48-
49-
## Comparison with Monads
50-
51-
One can use monads for these tasks, and some people do. For instance
52-
the `Reader` monad is used to abstract over accessing one entry in the
53-
environment. But the code for doing so quickly becomes complex and
54-
inefficient, in particular when combining several contextual
55-
accesses. Monads don't compose in general, and therefore even simple
56-
combinations need to be expressed on the level of monad transformers,
57-
at the price of much boilerplate and complexity. Recognizing this,
58-
peaple have recently experimented with free monads, which alleviate
59-
the composibility problem, but at the price of introducing a whole new
60-
level of interpretation.
61-
6243
## Implicit Parameters
6344

6445
In a functional setting, the inputs to a computation are most
@@ -67,7 +48,7 @@ functions to take additional parameters that represent configurations,
6748
capabilities, dictionaries, or whatever contextual data the functions
6849
need. The only downside with this is that often there's a large
6950
distance in the call graph between the definition of a contextual
70-
element and the site where it is used. Conseuqently, it becomes
51+
element and the site where it is used. Consequently, it becomes
7152
tedious to define all those intermediate parameters and to pass them
7253
along to where they are eventually consumed.
7354

@@ -122,11 +103,11 @@ a newly created transaction:
122103
trans.commit()
123104
}
124105

125-
The current transaction needs to be passed along a calling chain to all
106+
The current transaction needs to be passed along a call chain to all
126107
the places that need to access it. To illustrate this, here are three
127108
functions `f1`, `f2` and `f3` which call each other, and also access
128109
the current transaction. The most convenient way to achieve this is
129-
passing the current transaction as an implicit parameter.
110+
by passing the current transaction as an implicit parameter.
130111

131112
def f1(x: Int)(implicit thisTransaction: Transaction): Int = {
132113
thisTransaction.println(s"first step: $x")
@@ -165,6 +146,215 @@ Two sample calls of the program (let's call it `TransactionDemo`) are here:
165146
scala TransactionDemo 1 2 3 4
166147
aborted
167148

149+
So far, so good. The code above is quite compact as far as expressions
150+
are concerned. In particular, it's nice that, being implicit
151+
parameters, none of the transaction values had to be passed along
152+
explicitly in a call. But on the definition side, things are less
153+
rosy: Every one of the functions `f1` to `f3` needed an additional
154+
implicit parameter:
155+
156+
(implicit thisTransaction: Transaction)
157+
158+
A three-times repetition might not look so bad here, but it certainly
159+
smells of boilerplate. In real-sized projects, this can get much worse.
160+
For instance, the _dotty_ compiler uses implicit abstraction
161+
over contexts for most of its parts. Consequently it ends up with currently
162+
no fewer than 2641 occurrences of the text string
163+
164+
(implicit ctx: Context)
165+
166+
It would be nice if we could get rid of them.
167+
168+
## Implicit Functions
169+
170+
Let's massage the definition of `f1` a bit by moving the last parameter section to the right of the equals sign:
171+
172+
def f1(x: Int) = { implicit thisTransaction: Transaction =>
173+
thisTransaction.println(s"first step: $x")
174+
f2(x + 1)
175+
}
176+
177+
The right hand side of this new version of `f1` is now an implicit
178+
function value. What's the type of this value? Previously, it was
179+
`Transaction => Int`, that is, the knowledge that the function has an
180+
implicit parameter got lost in the type. The main extension implemented by
181+
the pull request is to introduce implicit function types that mirror
182+
the implicit function values which we have already. Concretely, the new
183+
type of `f1` is:
184+
185+
implicit Transaction => Int
186+
187+
Just like the normal function type syntax `A => B`, desugars to `scala.Function1[A, B]`
188+
the implicit function type syntax `implicit A => B` desugars to `scala.ImplicitFunction1[A, B]`.
189+
The same holds at other function arities. With dotty's [pull request #1758](https://github.com/lampepfl/dotty/pull/1758)
190+
merged there is no longer an upper limit of 22 for such functions.
191+
192+
The type `ImplicitFunction1` can be thought of being defined as follows:
193+
194+
trait ImplicitFunction1[-T0, R] extends Function1[T0, R] {
195+
override def apply(implicit x: T0): R
196+
}
197+
198+
However, you won't find a classfile for this trait because all implicit function traits
199+
get mapped to normal functions during type erasure.
200+
201+
There are two rules that guide type checking of implicit function types.
202+
The first rule says that an implicit function is applied to implicit arguments
203+
in the same way an implicit method is. More precisely, if `t` is an expression
204+
of an implicit function type
205+
206+
t: implicit (T1, ..., Tn) => R
207+
208+
such that `t` is not an implicit closure itself and `t` is not the
209+
prefix of a call `t.apply(...)`, then an `apply` is implicitly
210+
inserted, so `t` becomes `t.apply`. We have already seen that the
211+
definition of `t.apply` is an implicit method as given in the
212+
corresponding implicit function trait. Hence, it will in turn be
213+
applied to a matching sequence of implicit arguments. The end effect is
214+
that references to implicit functions get applied to implicit arguments in the
215+
same way as references to implicit methods.
216+
217+
The second rule is the dual of the first. If the expected type
218+
of an expression `t` is an implicit function type
219+
220+
implicit (T1, ..., Tn) => R
221+
222+
then `t` is converted to an implicit closure, unless it is already one.
223+
More precisely, `t` is mapped to the implicit closure
224+
225+
implicit ($ev1: T1, ..., $evn: Tn) => t
226+
227+
The parameter names of this closure are compiler-generated identifiers
228+
which should not be accessed from user code. That is, the only way to
229+
refer to an implicit parameter of a compiler-generated function is via
230+
`implicitly`.
231+
232+
It is important to note that this second conversion needs to be applied
233+
_before_ the expression `t` is typechecked. This is because the
234+
conversion establishes the necessary context to make type checking `t`
235+
succeed by defining the required implicit parameters.
236+
237+
There is one final tweak to make this all work: When using implicit parameters
238+
for nested functions it was so far important to give all implicit parameters
239+
of the same type the same name, or else one would get ambiguities. For instance, consider the
240+
following fragment:
241+
242+
def f(implicit c: C) = {
243+
def g(implicit c: C) = ... implicitly[C] ...
244+
...
245+
}
246+
247+
If we had named the inner parameter `d` instead of `c` we would
248+
have gotten an implicit ambiguity at the call of `implicitly` because
249+
both `c` and `d` would be eligible:
250+
251+
def f(implicit c: C) = {
252+
def g(implicit d: C) = ... implicitly[C] ... // error!
253+
...
254+
}
255+
256+
The problem is that parameters in implicit closures now have
257+
compiler-generated names, so the programmer cannot enforce the proper
258+
naming scheme to avoid all ambiguities. We fix the problem by
259+
introducing a new disambiguation rule which makes nested occurrences
260+
of an implicit take precedence over outer ones. This rule, which
261+
applies to all implicit parameters and implicit locals, is conceptually
262+
analogous to the rule that prefers implicits defined in companion
263+
objects of subclasses over those defined in companion objects of
264+
superclass. With that new disambiguation rule the example code above
265+
now compiles.
266+
267+
That's the complete set of rules needed to deal with implicit function types.
268+
269+
## How to Remove Boilerplate
270+
271+
The main advantage of implicit function types is that, being types,
272+
they can be abstracted. That is, one can define a name for an implicit
273+
function type and then use just the name instead of the full type.
274+
Let's revisit our previous example and see how it can be made more
275+
concise using this technique.
276+
277+
We first define a type `Transactional` for functions that take an implicit parameter of type `Transaction`:
278+
279+
type Transactional[T] = implicit Transaction => T
280+
281+
Making the return type of `f1` to `f3` a `Transactional[Int]`, we can
282+
eliminate their implicit parameter sections:
283+
284+
def f1(x: Int): Transactional[Int] = {
285+
thisTransaction.println(s"first step: $x")
286+
f2(x + 1)
287+
}
288+
def f2(x: Int): Transactional[Int] = {
289+
thisTransaction.println(s"second step: $x")
290+
f3(x * x)
291+
}
292+
def f3(x: Int): Transactional[Int] = {
293+
thisTransaction.println(s"third step: $x")
294+
if (x % 2 != 0) thisTransaction.abort()
295+
x
296+
}
297+
298+
You might ask, how does `thisTransaction` typecheck, since there is no
299+
longer a parameter with that name? In fact, `thisTransaction` is now a
300+
global definition:
301+
302+
def thisTransaction: Transactional[Transaction] = implicitly[Transaction]
303+
304+
You might ask: a `Transactional[Transaction]`, is that not circular? To see more clearly, let's expand
305+
the definition according to the rules given in the last section. `thisTransaction`
306+
is of implicit function type, so the right hand side is expanded to the
307+
implicit closure
308+
309+
implicit ($ev0: Transaction) => implicitly[Transaction]
310+
311+
The right hand side of this closure, `implicitly[Transaction]`, needs
312+
an implicit parameter of type `Transaction`, so the closure is further
313+
expanded to
314+
315+
implicit ($ev0: Transaction) => implicitly[Transaction]($ev0)
316+
317+
Now, `implicitly` is defined in `scala.Predef` like this:
318+
319+
def implicitly[T](implicit x: T) = x
320+
321+
If we plug that definition into the closure above and simplify, we get:
322+
323+
implicit ($ev0: Transaction) => $ev0
324+
325+
So, `thisTransaction` is just the implicit identity function on `transaction`!
326+
In other words, if we use `thisTransaction` in the body of `f1` to `f3`, it will
327+
pick up and return the unnamed implicit parameter that's in scope.
328+
329+
Finally, here are the `transaction` and `main` method that complete
330+
the example. Since `transactional`'s parameter `op` is now a
331+
`Transactional`, we can eliminate the `Transaction` argument to `op`
332+
and the `Transaction` lambda in `main`; both will be added by the compiler.
333+
334+
def transaction[T](op: Transactional[T]) = {
335+
implicit val trans: Transaction = new Transaction
336+
op
337+
trans.commit()
338+
}
339+
def main(args: Array[String]) = {
340+
transaction {
341+
val res = f1(args.length)
342+
println(if (thisTransaction.isAborted) "aborted" else s"result: $res")
343+
}
344+
}
345+
346+
## Categorically Speaking
347+
348+
There are many interesting connections with category theory to explore
349+
here. On the one hand, implicit functions are used for tasks that are
350+
sometimes covered with monads such as the reader monad. There's an
351+
argument to be made that implicits have better composability than
352+
monads and why that is.
168353

354+
On the other hand, it turns out that implicit functions can also be
355+
given a co-monadic interpretation, and the interplay between monads and
356+
comonads is very interesting in its own right.
169357

358+
But these discussions will have to wait for another time, as
359+
this blog post is already too long.
170360

0 commit comments

Comments
 (0)