Throwing

Sometimes, deeply nested expressions need to exit early from a computation, or pass a value directly back up to a higher computation without further processing.

Wolf implements a non-viral, strictly-typed “throwing” and “catching” mechanism to allow nested expressions to exit early.

Catching

A tuple can be prefixed with catch. This indicates that expressions inside of the tuple can override what the catch tuple evaluates to by “throwing” values to it.

let can_catch = catch (2)

catch does not change what the tuple evaluates to by default.

-- These two expressions are identical.
let foo = (2)
let foo = catch (2)

Throwing

While inside of a catch tuple, any expression can start with throw. This short-circuits the rest of the computation and throws the value to the nearest ancestor catch tuple.

let three = catch (
	-- 4 and 5 are not evaluated.
	1 -> 2 -> throw 3 -> 4 -> 5
)

Throwing is strictly local; you cannot throw to a catch tuple elsewhere in the source code. In particular, the call stack is not considered.

-- This is not allowed.
let foo = fn(message : str) throw message

let bar = catch ( foo("Hello world") )

Even if the whole call stack is locally known, it is not considered at all. Throws are explicitly lexically scoped.

-- `bar` becomes "Hello, world".
let bar = catch (
	-- This `throw` will go straight to bar's `catch` immediately.
	let foo = fn(message : str) throw message

	-- garb's `catch` tuple can only be discovered via the call stack, so it's not considered. 
	let garb = catch (
		foo("Hello, world") -- immediately throws to `bar`
	)
	-- This value will never be returned as a result.
	"Goodbye, world"
)