Skip to content

Effects

Sooner or later, a program has to talk to the outside world: print something, read the clock, call a service, write to a database.

The usual options each have a cost. Call the outside world directly, and the code is hard to test — there’s no seam to put a fake in. Pass the outside world in as parameters, and every function in between has to carry them along, whether it uses them or not.

Bridje’s answer is effects: named, typed values that code can use without passing them around, and that a caller can replace for everything it calls.

defx: declares an effect. It looks like a decl: — a name and a type — optionally followed by a default:

defx: now() Int // no default
defx: log(Str) Str println // defaults to println

log is a function from a Str to a Str — it hands back what it was given, as println does — and unless told otherwise, it prints.

Code that uses an effect just calls it, like any other function:

decl: .completedAt Int
decl: [a] completeTodo({& a}) {.completedAt & a}
def: completeTodo(todo)
log("completing a todo")
with(todo, .completedAt now())

Its type reads: given any record a, completeTodo returns that same record, now carrying .completedAt too.

There’s no extra parameter, and nothing special about the call. Bridje works out for itself which effects each function uses — completeTodo uses log and now — and so, in turn, does every function that calls it.

withFx: provides effects for everything evaluated inside it — however deep the calls go:

decl: fixedClock() Int
def: fixedClock()
1000
decl: [a] quietly(a) a
def: quietly(msg)
msg
withFx: [now fixedClock, log quietly]
completeTodo(milk)

Inside the withFx:, completeTodo sees a clock that always says 1000, and a log that prints nothing. Outside it, nothing has changed. A one-off implementation can be written in place, too: withFx: [now fn: fixedClock() 1000].

An effect’s implementation can use other effects — a log that writes via some lower-level stdio effect, say — and then that’s what the code inside the withFx: uses instead.

An effect with no default has to be provided by some enclosing withFx: before it’s called.

This is where effects earn their keep. Because the clock is an effect, a test can pin it. Here, todo.core holds completeTodo, milk and the now effect; the test refers to them through the alias c, which the next page explains:

ns: todo.core-test
require:
brj:
as(test, t)
todo:
as(core, c)
def: fixedClock()
1000
^t/.test
def: completingSetsTheTime()
withFx: [c/now fixedClock]
t/is(eq(1000, c/.completedAt(c/completeTodo(c/milk))))

No mocking library, no dependency injection framework, and nothing in completeTodo had to change to make it testable.

The last stop on the tour: namespaces and Java — organising a program, and using the JVM’s libraries from it.