Effect example: File System

In the Haskell community, there are many effect systems positioning themselves as an alternative to the mainstream MTL. The File System effect example is often used as their demonstrator of reinterpretation.

This is how it looks in Turbolift:

Definition

1. Imports

import turbolift.{!!, Signature, Effect, Handler}
import turbolift.effects.{State, Error}

2. Define the signature

trait FileSystemSignature extends Signature:
  def readFile(path: String): String !! ThisEffect
  def writeFile(path: String, contents: String): Unit !! ThisEffect

3. Define the effect type

trait FileSystemEffect extends Effect[FileSystemSignature] with FileSystemSignature:
  // Boilerplate:
  final override def readFile(path: String) = perform(_.readFile(path))
  final override def writeFile(path: String, contents: String) = perform(_.writeFile(path, contents))

4. Define a handler

Instantiate custom FileError effect, that we will be using in the handler:

case object FileError extends Error[FileErrorCause]
type FileError = FileError.type

enum FileErrorCause:
  case NoSuchFile(path: String)

  def message = this match
    case NoSuchFile(path) => s"No such file found: $path"

The handler itself:

extension (fx: FileSystemEffect)
  def inMemoryHandler =
    // Internal state:
    case object S extends State[Map[String, String]]
    type S = S.type

    // Our proxy depends on 2 effects: `S` and `FileError`
    new fx.impl.Proxy[S & FileError] with FileSystemSignature:
      override def readFile(path: String) =
        S.gets(_.get(path)).flatMap {
          case Some(contents) => !!.pure(contents)
          case None => FileError.raise(FileErrorCause.NoSuchFile(path))
        }

      override def writeFile(path: String, contents: String) =
        S.modify(_.updated(path, contents))

    .toHandler
    .partiallyProvideWith[FileError](S.handler(Map()).dropState)

Our handler has 2 dependencies: S and FileError effects. Using partiallyProvideWith method, we modify the handler in such a way, that dependency on S is removed (satisfied), but dependency on FileError remains. This way, we hide handler’s internal state from the outside world. Responsibility to handle the error effect though, is passed on the user.


Usage

Instantiate the effect

case object MyFS extends FileSystemEffect

// Optional:
type MyFS = MyFS.type

Run a program using the effect & handler

val program =
  for
    _ <- MyFS.writeFile("hello.txt", "Hello world!")
    contents <- MyFS.readFile("hello.txt")
  yield contents
// program: Computation[String, ThisEffect] = turbolift.Computation@3b13e7d2

val result = program
  .handleWith(MyFS.inMemoryHandler)
  .handleWith(FileError.handler)
  .run
// result: Either[FileErrorCause, String] = Right(value = "Hello world!")