Every useful program reaches outside itself: it reads a file, writes to a database, throws an exception, or draws a pixel. We call these side effects — things a function does beyond returning a value.
The problem is that ordinary type systems ignore them. A function readUser(id: Int) -> User promises a User, but says nothing about whether it hits the network, mutates global state, or crashes. All of that is hidden in the implementation. You discover it at runtime — or worse, in production.
Effect systems fix this by extending the type checker to track effects alongside values. Instead of just Int -> String, you write Int -> String ! {IO, Throw}, where the annotation ! {IO, Throw} is the effect row — a set declaring exactly what the function is allowed to do. The type checker then verifies that every call site permits those effects, and that functions claiming to be pure really are.
This idea was formalized in the late 1980s by Gifford and Lucassen (1988), and has since surfaced in languages like Koka (effect rows, 2014), Effekt, OCaml 5 (effects + continuations), and as an influence on Rust's async/Send/Sync traits. Related ideas: the Halting Problem shows some properties can never be checked statically, while program synthesis explores generating correct programs from specs.
Comments
Loading comments...