tracking

Effect tracking: `!io` in a signature, and the compiler holding you to it.

examples/effects/tracking.dawn

# Effect tracking: `!io` in a signature, and the compiler holding you to it.
#
# This is the half of the effect system that is always on. Nothing here
# installs a handler -- that is handlers.dawn, next door. The point of keeping
# the two apart is that they answer different questions: this file is about
# what a signature *promises*, that one is about who *answers* it.
#
# Try deleting `!io` from `report` and the error names the contaminating call.
#
# Run: dawn run examples/effects/tracking.dawn

## Pure: the same score always gives the same letter, and nothing is observed.
fn classify(score: Int) -> String =
  match score {
    s if s >= 90 -> "A"
    s if s >= 60 -> "pass"
    _ -> "fail"
  }

## Not pure, and it has to say so: `println` is `!io` and effects travel up.
fn report(name: String, score: Int) -> Unit !io =
  println("$name: $score -> ${classify(score)}")

## An effect *variable* is how a higher-order function stays honest without
## picking a side: `each` is pure when `f` is pure and `!io` when `f` is not.
## It is the reason `map` and `fold` need no io-flavoured twins.
fn each[T](xs: List[T], f: fn(T) -> Unit !e) -> Unit !e = {
  for x in xs {
    f(x)
  }
}

pub fn main() -> Unit !io = {
  each([("alice", 95), ("bob", 61), ("carol", 3)], entry => {
    let (name, score) = entry
    report(name, score)
  })
}

test "classification is a pure function, so a test needs no scaffolding" {
  assert classify(90) == "A"
  assert classify(89) == "pass"
  assert classify(59) == "fail"
}

test "a pure `f` keeps `each` pure -- this block declares no effect at all" {
  # No accumulator: a lambda captures by value, so counting things up from
  # inside one is a compile error and `fold` is the answer to that question.
  each([1, 2, 3], n => { assert n > 0 })
  assert fold([1, 2, 3], 0, (a, b) => a + b) == 6
}