Effect Safety
Similar to Java’s checked exceptions, Effekt features an effect system that allows us to track effects. As a very simple example, we can define our own exceptions (even though they are defined in the stdlib):
effect exc(msg: String): Nothing
def div(n: Double, m: Double): Double / { exc } =
if (m == 0.0) do exc("Division by zero") else n / m
While the left component of the return type (that is, Double
) is the type of values returned by
div
, its right component (that is, { exc }
)
describes the set of effects that need to be handled by callers of div
.
Effects are Requirements
As opposed to other effect systems, where effects communicate the side effects a program has besides computing a result, the notion of effects in the Effekt language is that of a requirement.
We thus read the signature of sayHello
as
“The function
div
computes a value of typeDouble
requiring a capability forexc
in its calling context.”
That is, it can only be run in contexts that allow the exc
effect operation.
For instance, we cannot call it in a “pure” function:
def pureFun(): Double / {} = div(42.0, 3.0)
// ^^^^^^^^^^^^^^
// Effect Exc is not allowed in this context.
Here, we put “pure” in quotes since the difference in our notion of effects also
leads to a difference of purity compared to existing languages.
Here, pureFun
simply imposes no requirements on its calling context. It would
thus be unsound to call div
, since it requires Exc
.
Higher-Order Functions and Relative Purity
Functions can take blocks as arguments. One example function that does so is
each
:
effect exc(msg: String): Nothing
def div(n: Double, m: Double): Double / { exc } =
if (m == 0.0) do exc("Division by zero") else n / m
def foreach[A](l: List[A]) { f: A => Unit }: Unit = <>
The type of the block argument f
is A => Unit
indicating that it consumes an
element of type A
. Actually, A => Unit
is
syntactic sugar for (A) => Unit / {}
. That is, it is a block from A
to Unit
not using any effects visible to each
. The implementation of each
is omitted,
we are using a hole (<>
) that allows type-checking, but will result in a
runtime error, when forced.
Maybe surprisingly, at the callsite to each
, we actually can use other effects:
def meanRatios(l: List[(Double, Double)]): Double / { exc } = {
var sum = 0.0;
var count = 0;
l.foreach {
case (x, y) =>
sum = sum + div(x, y)
// ^^^^^^^^^
// This could use the Exc effect!
count = count + 1
}
div(sum, count.toDouble)
}
Here, the block passed to foreach
does use another effect, namely the exc
effect operation. In fact, Effekt encourages a different reading of signatures of effectful
functions, similar to “contracts”:
// "provided" effects
// vv
def foreach[A](l: List[A]) { f: A => Unit / {} }: Unit / {}
// ^^
// "required" effects
That is, effects in return position are “required” (the calling context needs to provide them), and effects in argument positions are “provided” (the function passed in the calling context can use them).
This shift in perspective offers guarantees for the caller of foreach
: the
implementation cannot observe (besides global side channeling) any effects
used by f
.
This comes at the cost of some guarantees for the implementor of foreach
:
Calling f
might have side-effects observable to the user, losing referential
transparency to some extent. We cannot simply replace a call to f
by its
results. Also, calling it twice is different from calling it once.
Even though f
is annotated with the empty effect set, it thus cannot be
assumed to be “pure”.
We will see this in detail when talking about effect polymorphism.
We can run the example (we’ll cover handling in the next section) in the following way:
try { meanRatios([(4.2, 1.3)]) } with exc { msg => 0.0 }