Tracking Side Effects in Scala
What if we’d use Scala’s type system for tracking side-effects in impure code, too? In the Scala/FP community we use and love effect systems, such as Cats Effect with its IO
data type. “Suspending side-effects” in IO
is great, but in Scala it’s either IO
or forgoing any kind of type-safety for side-effects, and that’s bad.
In spite of our wishes to the contrary, integration code or low-level code in Scala is inevitable. Also, there are instances in which impure/imperative code is actually clearer. I still have the feeling that Cats-Effect’s Ref
is harder to understand in practice than AtomicReference
, for example, in spite of all benefits of referential transparency or the decoupling that happens between the declarations of expressions and their evaluation. And software-transactional memory can be worse than Ref
, which is probably why in Scala it never took off, in spite of decent implementations available.
For impure code, I’d still like to notice the side effects, visually, via types, and I still want the compiler to protect me from leaks. Scala 3 makes this easier, via Context Functions.
// SCALA 3
import scala.io.StdIn
// https://dotty.epfl.ch/docs/reference/experimental/erased-defs
// import scala.language.experimental.erasedDefinitions
/*erased*/ class CanSideEffect
/*erased*/ class CanBlockThreads extends CanSideEffect
type UnsafeIO[+A] = CanSideEffect ?=> A
type UnsafeBlockingIO[+A] = CanBlockThreads ?=> A
def unsafePerformIO[A](f: UnsafeIO[A]): A =
f(using new CanSideEffect)
def unsafePerformBlockingIO[A](f: UnsafeBlockingIO[A]): A =
f(using new CanBlockThreads)
//-------
object Console {
def writeLine(msg: String): UnsafeBlockingIO[Unit] =
println(msg)
def readLine: UnsafeBlockingIO[String] =
StdIn.readLine
}
@main def main(): Unit = {
unsafePerformBlockingIO {
Console.writeLine("Enter your name:")
val name = Console.readLine
Console.writeLine(s"Hello, $name!")
}
}
We might also make use of erased definitions, to eliminate any overhead, but this is an experimental feature that’s only available in nightly builds.
Note that this is possible to do in Scala 2, but less nice, although in libraries we might think of developing for the future, without leaving users of Scala 2 in the dust.
// SCALA 2
class CanSideEffect
class CanBlockThreads extends CanSideEffect
type UnsafeIO[+A] = CanSideEffect => A
type UnsafeBlockingIO[+A] = CanBlockThreads => A
def unsafePerformIO[A](f: UnsafeIO[A]): A =
f(new CanSideEffect)
def unsafePerformBlockingIO[A](f: UnsafeBlockingIO[A]): A =
f(new CanBlockThreads)
//-------
object Console {
def writeLine(msg: String)(implicit permit: CanBlockThreads): Unit =
println(msg)
def readLine(implicit permit: CanBlockThreads): String =
StdIn.readLine
}
object Main extends App {
unsafePerformBlockingIO { implicit permit =>
Console.writeLine("Enter your name:")
val name = Console.readLine
Console.writeLine(s"Hello, $name!")
}
}
Scala 3 also introduced an experimental feature for dealing with “checked exceptions” that makes use of this mechanism, see the “safer exceptions” PR
There is precedent in Scala 2 for using implicits like this. For example, the Await
utilities that we use for Future
:
import scala.concurrent._
import scala.concurrent.duration._
//...
Await.result(future, 10.seconds)
The implementation of result
is this one:
object Await {
// ...
def result[T](awaitable: Awaitable[T], atMost: Duration): T =
blocking(awaitable.result(atMost)(AwaitPermission))
}
This is making use of Scala’s “blocking context”, being decoupled from Scala’s Future
implementation via this interface:
trait Awaitable[+T] {
//...
def result(atMost: Duration)(implicit permit: CanAwait): T
}
In other words, the implementation forces the use of Await.result
via a CanAwait
implicit (which can’t be instantiated from user code). This is also how Monix disallows blocking I/O methods to run on top of JavaScript (such as Task.runSyncUnsafe) 😉
Another API that I can remember is Scala-STM, a project that I used back in the day, and that looks like this:
def addLast(elem: Int): Unit =
atomic { implicit txn =>
val p = header.prev()
val newNode = new Node(elem, p, header)
p.next() = newNode
header.prev() = newNode
}
The implementation is keeping track of the current transaction, at compile-time, via that implicit parameter. Although, in this case the implicit isn’t just a “compile-time proof”, but rather something that affects the runtime too.
I think I’m being inspired by Martin Odersky’s presentation:
Do you think tracking side effects like this would be useful in libraries? Monix, for example, also contains impure parts, and could make use of it.
Would you like it? Any prior art that I’m not aware of?