Lightweight Effect Polymorphism
Tracking effects statically in the type is a great way to achieve safety. However, often, as soon as you start writing higher-order functions the type- and effect system starts to get into your way.
Effect Polymorphism
Not so in Effekt! It features a new form of effect polymorphism, which we call contextual effect polymorphism.
import list
effect Break(): Unit
type File = List[String]
def eachLine(file: File) { f: String => Unit / {} }: Unit / {} =
file.foreach { line => f(line) }
val someFile = ["first line", "second line", "", "third line"]
def printLines(file: File) = try {
eachLine(file) { line =>
if (line == "") { do Break() }
else { println(line) }
}
} with Break {
println("found an empty line, aborting!")
}
The higher-order function eachLine
expects two arguments: firstly, a
value argument, which is passed in parenthesis.
Secondly, a block argument, which is passed in curly braces.
It calls the provided block with each line in the file.
The signature of eachLine
in Effekt is:
def eachLine(file: File) { f: String => Unit / {} }: Unit / {}
Note that the signature does not say anything about effects.
The Traditional Reading
In traditional effect systems, this would be read is:
“given a block
f
that has no effects,eachLine
also has no effects”.
Given that reading, we couldn’t use eachLine
as in our example, since the block passed
to eachLine
uses two effects Break
and Logger
(due to println
).
The Contextual Reading
In Effekt, we apply a different reading, which we call contextual:
“given a block
f
without any further requirements,eachLine
does not require any effects.”
The function eachLine
is effect polymorphic. We can call it with arbitrary
blocks, indepedent of the effects the block arguments use.
The type ofthe block parameter f
tells us that it does not impose any
requirements on its caller within eachLine
.
However, it can have effects, that simply need to be handled at the call-site
of eachLine
.
Contextual Effect Polymorphism by Example
Here are some examples of calling eachLine
. For example, we can provide a block that does not use any effects:
eachLine(someFile) { line => () }
The type of the block is String => Unit / {}
and
the overall type of the call is Unit / {}
.
Further, given the Logger
effect
interface Logger {
def log(msg: String): Unit
}
we can provide a block that uses it:
eachLine(someFile) { line => do log(line) }
The type of the block is String => Unit / { Logger }
and
the overall type of the call now becomes Unit / { Logger }
.
Since it still has unhandled user effects, it does not type-check and we cannot run it.
To be able to run it, we handle the user-effect at the call-site to eachLine
, for example
by printing to the console:
def printLogger[R] { prog: () => R / Logger }: R =
try { prog() } with Logger { def log(msg) = resume(println(msg)) }
printLogger {
eachLine(someFile) { line => do log(line) }
}
Now, the effects that are used by the block argument are handled in its lexical scope.