|
You can think of algebraic effects kind of like: From callsite Foo, I call out to a function with a known name, Bar, with one or more parameters. The runtime (or sufficiently smart compiler) searches upwards in scope, for a handler Baz that provides the function with that name. Baz's Bar is then called, with both the provided parameters, and, crucially, a function that is "the rest of Foo." So, an implementation of Exception with effects would ignore the resume, and look something like: define_effect Exception {
// only one function provided by Exception, but could have more
throw(String)
}
define_handler printExceptions {
throw(msg, resume_func): { println(msg) }
}
define_func getPage(url) {
request = http.get(url)
if not request.ok { throw("Could not download") }
return page
}
// main entrypoint
withHandlers printExceptions {
page = getPage("https://cheese.com")
println(page.text)
}
But you could also write an "on error resume next" handler for Exception. Exceptions thrown with this handler would be equivalent to toss. (in real life you'd probably write a different effect, rather than re-using the Exception/throw effect): define_handler onErrorResumeNext {
throw(msg, resume_func): {
println(msg)
// YOLO, call it anyways
withHandlers onErrorResumeNext {
resume_func()
}
}
}
withHandlers onErrorResumeNext {
page = getPage("https://cheese.com")
println(page.text) // Probably uninitialized - a better :P
}
Breaking away from the Exception example, two cool examples are: // Stream - potentially infinite and/or asynchronous iterables
// basically just like python generators
define_effect Stream {
emit(item)
}
define_handler toList(accumulator) {
emit(item, resume_func): {
if item == nil {
return accumulator
} else {
accumulator.append(item)
withHandlers toList { resume_func() }
}
}
}
define_handler take(how_many) {
emit(item, resume_func) {
if how_many == 0 {
emit nil
} else {
withHandlers take(how_many-1) { resume_func() }
}
}
}
define_func fib_stream() {
a, b = 0, 1
loop {
emit a
a, b = b, a+b
}
}
// Usage
first_five = withHandlers toList {
withHandlers take(5) {
fib()
}
}
println("The first five fibonacci numbers are", first_five)
And this one I'm just going to lift from the Unison documentation [1]: Each.toList do
a = Each.range 0 5
-- beginning of resume_func f_A
b = each [1, 2, 3]
-- beginning of resume_func f_B
guard (a < b)
-- beginning of resume_func f_C
(a, b)
-- yields
[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]
Note that this has the same semantics as: results = []
for (a = 0; a < 5 ; a++) { // for each a, call f_A(a)
foreach b in [1, 2, 3] { // for each b, call f_B(b)
if not a<b { continue } // if a<b, call f_C(a, b)
results.append((a, b))
}
}
And it is possible to do this, because these resumable functions can be resumed an arbitrary number of times - it's up to the handler. They also can pass parameters back to the resume_func.Exceptions/hurl = exactly 0 resumptions
toss = exactly 1 resumption
effects = 0..many resumptions [1]: https://share.unison-lang.org/@unison/base/code/releases/3.5... |