Conditions.jl is a proof-of-principle implementation of a Common Lisp like condition system.
In contrast to throwing an exception, signalling a condition does not unwind the stack. This allows a condition handler to resume execution, e.g.,
using Conditions
function seq()
n = 1
while n < 10
@signal n # Think: `yield n`
n += 1
end
end
handler_bind(Handler(Any, c -> @show c)) do
seq()
end
See the package README for more examples and how restarts allow more elaborate error recovery than try-catch.
Looks great to me and adds a really nice feature from Common Lisp I have somewhat missed in Julia!
I also expected something like this to pop up soon since special bindings where just merged recently. However you don’t seem to use them and instead rely on task_local_storage. This has the limitation that handlers can’t be set across tasks but it would just be a small change to switch to ScopedValues as storage instead
Thanks, had not seen ScopedValues and thus took task_local_storage for special variables. Its certainly interesting if handlers could cross task boundaries … have not thought about the implications though.
Ok, have switched to ScopedValues … this is pretty cool actually as you can now drive several tasks on different threads from a single handler:
using Conditions
function seq(n)
i = 1
while i < n
@restart_case @signal(i) begin
:next => () -> nothing
end
@show Threads.threadid()
i += 1
end
end
function donext(c)
sleep(0.01)
@show c
invoke_restart(find_restart(:next))
end
julia> @sync handler_bind(Handler(Any, donext)) do
Threads.@spawn seq(3)
Threads.@spawn seq(5)
end
c = 1
Threads.threadid() = 2
c = 1
Threads.threadid() = 5
c = 2
Threads.threadid() = 2
c = 2
Threads.threadid() = 5
c = 3
Threads.threadid() = 5
c = 4
Threads.threadid() = 5
Task (done)
This looks kind of cool, but I can’t immediately grasp from your examples what you’d actually use this for. For example, Infiltrator is mentioned in the readme, but does it make a difference whether that is run from inside a function or from the outside, after the detour of a signal?
Not sure if I understand you correctly, but the basic difference between throw and @signal is that with the former, you can only end up lower on the stack inside some try-catch form, whereas the latter leaves the stack untouched and a handler from handler_bind runs on top of the current stack. This can be used in several ways:
When toggle_interactive(true) there is a default handler which runs @infiltrate on top of the stack, i.e., you get dumped into a debug prompt exactly at the place where @signal was called (and not where it got caught/handled). It does not matter how you got there or where it was handled – if that’s what you meant with “does it make a difference whether that is run from inside a function or from the outside”.
Using @restart_case you can mark positions on the stack which can be used to resume execution later on. In particular, a handler lower on the stack can decide to continue execution of the program on one of such restart points. Practical Common Lisp has a slightly larger example showing why and how this can be used for error recovery. For illustration, I have just ported this example.