Skip to content

Latest commit

 

History

History
130 lines (91 loc) · 4.63 KB

File metadata and controls

130 lines (91 loc) · 4.63 KB

Effects

An effect is something a function does besides computing its result: printing, reading a file, calling a server, reading the clock. In bork, a function's signature lists its effects, and the compiler checks the list.

uses

fn total(prices: List[Int]): Int {
  prices.fold(0, (sum, p) => sum + p)
}

fn printTotal(prices: List[Int]) uses io {
  println(s"total: ${total(prices)}")
}

fn main() {
  printTotal([450, 1250])
}

total declares no effects, so it is pure: given the same arguments it gives the same result, and it does not touch the outside world. printTotal prints, so it says uses io.

uses comes after the parameters and before the result type. Several effects are joined with +: uses io + net.

The effects are:

Effect Meaning
io Files, standard output, environment variables, processes
net Network connections
clock Reading the time, sleeping, timers
random Random numbers
state Shared state that can change: atoms, channels, cancellation

A sixth effect, build, marks functions that read files during compilation. It is only allowed inside comptime.

The compiler checks the list

A function may only do what it declares. That includes what the functions it calls do.

fn total(prices: List[Int]): Int {
  println("adding up")
  prices.fold(0, (sum, p) => sum + p)
}
total uses io (it calls println), but its signature allows no effects; declare it: uses io

If a function has no uses, nothing it calls can read a file or use the network, however deep its call tree goes.

Three things are not tracked as effects: logging with bork/log, the development helper dbg, and panics. A pure function can do those. Go code is the other limit, since the effects of an unsafe go function are declared by its author.

main and tests may use every run-time effect without declaring them. If main does declare uses, it is held to it.

Functions passed as arguments

Methods such as map and forEach take a function. They do not have effects of their own: a call has whatever effects the function passed to it has.

fn lengths(words: List[String]): List[Int] {
  words.map(w => w.byteLength())
}

fn printAll(words: List[String]) uses io {
  words.forEach(w => println(w))
}

fn main() {
  println(lengths(["a", "bc"]))
  printAll(["a", "bc"])
}

lengths stays pure, because its lambda is pure. printAll needs uses io, because its lambda prints.

Your own functions work the same way. A function-typed parameter accepts a function with any effects, and the caller is charged for them. To accept only pure functions, write uses nothing in the parameter's type. To allow specific effects and no others, name them, as in (String) uses io => Ok.

fn twice(f: (Int) => Int, x: Int): Int {
  f(f(x))
}

fn twicePure(f: (Int) uses nothing => Int, x: Int): Int {
  f(f(x))
}

fn main() {
  println(twice(n => n + 1, 0))
  println(twicePure(n => n * 2, 1))
}

Ambient values

Some values belong to a whole request: a trace id, the signed-in user, a locale. An ambient value is an alternative to passing them through every function as a parameter. It is declared once, bound for a block of code, and read by the functions that say they need it.

ambient requestId: String

fn log(message: String) uses io needs requestId {
  println(s"[$requestId] $message")
}

fn handle(path: String) uses io needs requestId {
  log(s"handling $path")
}

fn main() {
  with (requestId: "req-42") {
    handle("/users")
  }
}
  • ambient requestId: String declares the value and its type.
  • needs requestId in a signature lets the function read it by name. It comes after uses.
  • with (requestId: ...) { ... } binds it for the block.

This is still checked. A function that needs a value can only be called from inside a with that binds it, or from another function that needs it. handle declares the need because it calls log. Leaving out the with in main is a compile error.

A function can treat a value as optional with needs locale?, and then reads it as an Option.

An ambient value can also be marked so that the runtime uses it. logged ambient adds the value to every log line written while it is bound. propagated("header-name") ambient sends it as a header on outgoing HTTP calls, and a bork server on the other side picks it up. The service_context example shows a trace id passing between two services this way.


Previous: Facts · Next: Scopes and tasks · All pages