Skip to content

Commit 30c7b73

Browse files
committed
More tests and starting a blog post
1 parent 022c2b1 commit 30c7b73

File tree

2 files changed

+288
-1
lines changed

2 files changed

+288
-1
lines changed
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
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 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 comonadic
11+
abstraction to Scala". That's quite a mouthful, so I better explain what I
12+
mean by it.
13+
14+
Let me try to explain the words in this sentence from right to left.
15+
16+
*Scala*: I assume everyone who reads this understands that we mean the
17+
programming language, not the opera house.
18+
19+
*Abstraction*: The ability to name a concept and use just the name afterwards.
20+
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
32+
33+
- passing configuration data to the parts of a system that need them,
34+
- managing capabilities for security critical tasks,
35+
- wiring components up with dependency injection,
36+
- defining the meanings of operations with type classes,
37+
- more generally, passing any sort of context to a computation.
38+
39+
Implicit function types are a suprisingly simple and general way to
40+
make coding patterns solving these tasks abstractable, reducing
41+
boilerplate code and increasing applicability.
42+
43+
*First Step* My pull request is first implementation. In solves the
44+
problem in principle, but it introduces some run-time overhead. The
45+
next step will be to eliminate the run-time overhead through some
46+
simple optimizations.
47+
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+
62+
## Implicit Parameters
63+
64+
In a functional setting, the inputs to a computation are most
65+
naturally expressed as _parameters_. One could simply augment
66+
functions to take additional parameters that represent configurations,
67+
capabilities, dictionaries, or whatever contextual data the functions
68+
need. The only downside with this is that often there's a large
69+
distance in the call graph between the definition of a contextual
70+
element and the site where it is used. Conseuqently, it becomes
71+
tedious to define all those intermediate parameters and to pass them
72+
along to where they are eventually consumed.
73+
74+
Implicit parameters solve one half of the problem. Implicit
75+
parameters do not have to be propagated using boilerplate code; the
76+
compiler takes care of that. This makes them practical in many
77+
scenarios where plain parameters would be too cumbersome. For
78+
instance, type classes would be a lot less popular if one would have
79+
to pass all dictionaries by hand. Implicit parameters are also very
80+
useful as a general context passing mechanism. For instance in the
81+
_dotty_ compiler, almost every function takes an implicit context
82+
parameter which defines all elements relating to the current state of
83+
the compilation. This is in my experience much better than the cake
84+
pattern because it is lightweight and can express context changes in a
85+
purely functional way.
86+
87+
The main downside of implicit parameters is the verbosity of their
88+
declaration syntax. It's hard to illustrate this with a smallish example,
89+
because it really only becomes a problem at scale, but let's try anyway.
90+
91+
Let's say we want to write some piece of code that's designed to run
92+
in a transaction. For the sake of illustration here's a simple transaction class:
93+
94+
class Transaction {
95+
private val log = new ListBuffer[String]
96+
def println(s: String): Unit = log += s
97+
98+
private var aborted = false
99+
private var committed = false
100+
101+
def abort(): Unit = { aborted = true }
102+
def isAborted = aborted
103+
104+
def commit(): Unit =
105+
if (!aborted && !committed) {
106+
Console.println("******* log ********")
107+
log.foreach(Console.println)
108+
committed = true
109+
}
110+
}
111+
112+
The transaction encapsulates a log, to which one can print messages.
113+
It can be in one of three states: running, committed, or aborted.
114+
If the transaction is committed, it prints the stored log to the console.
115+
116+
The `transaction` method lets one run some given code `op` inside
117+
a newly created transaction:
118+
119+
def transaction[T](op: Transaction => T) = {
120+
val trans: Transaction = new Transaction
121+
op(trans)
122+
trans.commit()
123+
}
124+
125+
The current transaction needs to be passed along a calling chain to all
126+
the places that need to access it. To illustrate this, here are three
127+
functions `f1`, `f2` and `f3` which call each other, and also access
128+
the current transaction. The most convenient way to achieve this is
129+
passing the current transaction as an implicit parameter.
130+
131+
def f1(x: Int)(implicit thisTransaction: Transaction): Int = {
132+
thisTransaction.println(s"first step: $x")
133+
f2(x + 1)
134+
}
135+
def f2(x: Int)(implicit thisTransaction: Transaction): Int = {
136+
thisTransaction.println(s"second step: $x")
137+
f3(x * x)
138+
}
139+
def f3(x: Int)(implicit thisTransaction: Transaction): Int = {
140+
thisTransaction.println(s"third step: $x")
141+
if (x % 2 != 0) thisTransaction.abort()
142+
x
143+
}
144+
145+
The main program calls `f1` in a fresh transaction context and prints
146+
its result:
147+
148+
def main(args: Array[String]) = {
149+
transaction {
150+
implicit thisTransaction =>
151+
val res = f1(args.length)
152+
println(if (thisTransaction.isAborted) "aborted" else s"result: $res")
153+
}
154+
}
155+
156+
Two sample calls of the program (let's call it `TransactionDemo`) are here:
157+
158+
scala TransactionDemo 1 2 3
159+
result: 16
160+
******* log ********
161+
first step: 3
162+
second step: 4
163+
third step: 16
164+
165+
scala TransactionDemo 1 2 3 4
166+
aborted
167+
168+
169+
170+

tests/run/implicitFuns.scala

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ object Contextual {
8181

8282
def runOn(f: java.io.File): Ctx[Int] = {
8383
val options = List("-verbose", "-explaintypes")
84-
process(f)(ctx.withBinding(Options, options))
84+
process(f).apply(ctx.withBinding(Options, options))
8585
}
8686

8787
def process(f: java.io.File): Ctx[Int] =
@@ -94,3 +94,120 @@ object Contextual {
9494
assert(!compile("a"))
9595
}
9696
}
97+
98+
import collection.mutable.ListBuffer
99+
100+
class Transaction {
101+
private val log = new ListBuffer[String]
102+
def println(s: String): Unit = log += s
103+
104+
private var aborted = false
105+
private var committed = false
106+
107+
def abort(): Unit = { aborted = true }
108+
def isAborted = aborted
109+
110+
def commit(): Unit =
111+
if (!aborted && !committed) {
112+
Console.println("******* log ********")
113+
log.foreach(Console.println)
114+
committed = true
115+
}
116+
}
117+
118+
object TransactionalExplicit {
119+
120+
def transaction[T](op: Transaction => T) = {
121+
val trans: Transaction = new Transaction
122+
op(trans)
123+
trans.commit()
124+
}
125+
126+
def f1(x: Int)(implicit thisTransaction: Transaction): Int = {
127+
thisTransaction.println(s"first step: $x")
128+
f2(x + 1)
129+
}
130+
def f2(x: Int)(implicit thisTransaction: Transaction): Int = {
131+
thisTransaction.println(s"second step: $x")
132+
f3(x * x)
133+
}
134+
def f3(x: Int)(implicit thisTransaction: Transaction): Int = {
135+
thisTransaction.println(s"third step: $x")
136+
if (x % 2 != 0) thisTransaction.abort()
137+
x
138+
}
139+
140+
def main(args: Array[String]) = {
141+
transaction {
142+
implicit thisTransaction =>
143+
val res = f1(args.length)
144+
println(if (thisTransaction.isAborted) "aborted" else s"result: $res")
145+
}
146+
}
147+
}
148+
149+
object Transactional {
150+
type Transactional[T] = implicit Transaction => T
151+
152+
def transaction[T](op: Transactional[T]) = {
153+
implicit val trans: Transaction = new Transaction
154+
op
155+
trans.commit()
156+
}
157+
158+
def thisTransaction: Transactional[Transaction] = implicitly[Transaction]
159+
160+
def f1(x: Int): Transactional[Int] = {
161+
thisTransaction.println(s"first step: $x")
162+
f2(x + 1)
163+
}
164+
def f2(x: Int): Transactional[Int] = {
165+
thisTransaction.println(s"second step: $x")
166+
f3(x * x)
167+
}
168+
def f3(x: Int): Transactional[Int] = {
169+
thisTransaction.println(s"third step: $x")
170+
if (x % 2 != 0) thisTransaction.abort()
171+
x
172+
}
173+
174+
def main(args: Array[String]) = {
175+
transaction {
176+
val res = f1(args.length)
177+
println(if (thisTransaction.isAborted) "aborted" else s"result: $res")
178+
}
179+
}
180+
}
181+
182+
object TransactionalExpansion {
183+
184+
def transaction[T](op: Transaction => T) = {
185+
val trans: Transaction = new Transaction
186+
op.apply(trans)
187+
trans.commit()
188+
}
189+
190+
def thisTransaction = $t: Transaction => $t
191+
192+
def f1(x: Int) = { $t: Transaction =>
193+
thisTransaction.apply($t).println(s"first step: $x")
194+
f2(x + 1).apply($t)
195+
}
196+
def f2(x: Int) = { $t: Transaction =>
197+
thisTransaction.apply($t).println(s"second step: $x")
198+
f3(x * x).apply($t)
199+
}
200+
def f3(x: Int) = { $t: Transaction =>
201+
thisTransaction.apply($t).println(s"third step: $x")
202+
if (x % 2 != 0) thisTransaction.apply($t).abort()
203+
x
204+
}
205+
206+
def main(args: Array[String]) = {
207+
transaction { $t =>
208+
val res = f1(args.length).apply($t)
209+
println(if (thisTransaction.apply($t).isAborted) "aborted" else s"result: $res")
210+
}
211+
}
212+
}
213+

0 commit comments

Comments
 (0)