begin is just one kind of block. try is also a block.
julia> ex = :(try
# do something
catch e
e::AssertionError -> println("AssertionError")
e::InexactError -> println("InexactError")
end);
julia> ex.head
:try
julia> ex.args[1]
quote
#= REPL[353]:3 =#
end
julia> ex.args[2]
:e
julia> ex.args[3]
quote
#= REPL[353]:4 =#
e::AssertionError->begin
#= REPL[353]:4 =#
println("AssertionError")
end
#= REPL[353]:5 =#
e::InexactError->begin
#= REPL[353]:5 =#
println("InexactError")
end
end
So we could just do the following which does nothing at the end.
julia> macro errdispatch(ex)
end
@errdispatch (macro with 1 method
julia> @errdispatch try
# do something
catch e
e::AssertionError -> println("AssertionError")
e::InexactError -> println("InexactError")
end
The plan here is
Locate the catch block, ex.args[3]
Find all the statements with a -> head and turn them info different methods of the same function.
Pass e to that function so we can use multiple dispatch.
macro error_dispatch(e)
_error_dispatch(e)
end
function _error_dispatch(ex)
catch_block = ex.args[3]
exception = ex.args[2]
catch_func = gensym(:catch)
catch_block.args = map(catch_block.args) do cex
if cex isa Expr && cex.head == :(->)
_anon_to_named_func(catch_func, cex)
else
cex
end
end
push!(catch_block.args, :($catch_func($exception)))
esc(ex)
end
function _anon_to_named_func(name::Symbol, anon::Expr)
@assert(anon.head == :(->))
func_args = anon.args[1]
func_body = anon.args[2]
quote
$name($func_args) = $func_body
end
end
Here is a demonstration:
julia> function foo(g)
@error_dispatch try
g()
catch e
e::AssertionError -> println("Hello. I got an AssertionError!")
e::InexactError -> println("Hola. ¡Recibí un IneaxctError!")
end
end
foo (generic function with 1 method)
julia> foo(()->@assert(false))
Hello. I got an AssertionError!
julia> foo(()->Int(5.2))
Hola. ¡Recibí un IneaxctError!
It works by converting all the anonymous functions created in the catch block into methods of the same named function. Then it adds a statement to call that named function with the exception.
julia> @macroexpand @error_dispatch try
g()
catch e
e::AssertionError -> println("Hello. I got an AssertionError!")
e::InexactError -> println("Hola. ¡Recibí un IneaxctError!")
end
:(try
#= REPL[81]:2 =#
g()
catch e
#= REPL[81]:4 =#
begin
#= REPL[74]:6 =#
var"##catch#310"(e::AssertionError) = begin
#= REPL[74]:6 =#
begin
#= REPL[81]:4 =#
println("Hello. I got an AssertionError!")
end
end
end
#= REPL[81]:5 =#
begin
#= REPL[74]:6 =#
var"##catch#310"(e::InexactError) = begin
#= REPL[74]:6 =#
begin
#= REPL[81]:5 =#
println("Hola. ¡Recibí un IneaxctError!")
end
end
end
var"##catch#310"(e)
end)
That’s a neat looking workaround. May I suggest omitting the e after catch? It’s slightly misleading, because it suggests that the e used in the patterns must be the same variable, but that’s not the case. I think I would like this more if the macro complained if there were an exception variable after catch. The macro body could add its own variable (to be passed to the freshly created handler function).
Actually I was thinking of heading in the opposite direction. Essentially, all that I’m doing is allowing one to create methods for an anonymous function. I was wondering it if would be better to be more explicitly like the following instead of doing “magic”.
try
catch e
error_handler = @anon_method begin
e::AssertionError -> println("AssertionError")
e::InexactError -> println("InexactError")
end
error_handler(e)
end
There is a very important difference with the macro I published above. It uses multiple dispatch rules and order does not matter. My sense at the moment is that Match.jl and Rematch.jl will match against the first pattern. My method above will use multiple dispatch rules and likely require dynamic multiple dispatch.
You’re perfectly right. How about this form then, to emphasize dispatch, but to reduce boilerplate?
try
catch e
@dispatch_error e begin
e::AssertionError -> println("AssertionError")
e::InexactError -> println("InexactError")
end
end
Update: It doesn’t have to be called @dispach_error, it could be @dispatch_lambda or plain @dispatch as well, since there is no error specific stuff in it.
And based on your implementation, here’s how I would do it:
macro dispatch(expr, body)
@assert(body isa Expr && body.head == :block,
"begin ... end block expected in second argument!")
fn = gensym()
body = map(body.args) do ex
if ex isa Expr && ex.head == :(->)
:($fn($(ex.args[1])) = $(ex.args[2]))
else
ex
end
end
return quote
let $fn
$(body...)
$fn($expr)
end
end |> esc
end
select case (n)
case (-1)
print *, 'negative one'
case (0)
print *, 'zero'
case (1)
print *, 'positive one'
case default
print *, 'other value'
end select
As long as we’re debating syntax, I would offer something like
y = try
sqrt(x)
catch e::DomainError
zero(x)
catch e::Union{MethodError, AssertionError}
println("unhelpful remark")
rethrow(e)
#= implicit rethrow for non-caught types
catch e::Any
rethrow(e)
=#
finally
println("at least it's over")
end
The catch statements would be considered until the first matching signature. In fact, I might go so far as to just write this as catch e isa DomainError rather than catch e::DomainError. In either case, this would be equivalent to something like
y = try
sqrt(x)
catch e
if e isa DomainError
oftype(x,NaN)
elseif e isa Union{MethodError, AssertionError}
println("unhelpful remark")
rethrow(e)
else # implicit rethrow for non-caught types
rethrow(e)
end
finally
println("at least it's over")
end
although I suppose that isn’t so awful to write long-form already. How much is really saved?
This would be nice if the semantics were to choose the first applicable catch. But if we want the most specific catch to apply, then something looking like dispatch (e.g., e::AssertionError -> println("AssertionError") in a suggestion above) would be more appropriate. Although I don’t like that the example uses anonymous functions as it’s tedious to make multi-line expressions with them (and the anonymous function extended syntax function (e::AssertionError) gets weird because I don’t know whether people are suggesting to make these literal functions – can you rethrow from a child scope? If you want actual functions, just make a named function and call it from the catch).
To be honest, I rarely write catch to handle more than one specific error type (and just rethrow the rest) so I don’t know which (“first” or “best” or something else) might be more useful. How much do people nitpick around multiple specific error types in the wild? Enough that this is even useful?
I don’t see how your suggestion is different from mine (your example makes a few details more explicit, but it seems to be the same otherwise), but as long as we’re on the same track, I’m happy with it.
Yeah, I’m definitely voting on classic dispatch semantics, i.e., the most specific applies. The reason being, this is the natural way for Julia.
Apparently, you can, as long as the code is running within a catch block. I have tested this both in my initial example and when I experimented with the @dispatch macro above.
Well, it has to be one way or the other, so again, I suggest use the one that better blends in with the rest of the language, so go for the best match.
I like the syntax you and @mikmoore proposed. I am thinking about the question of catching the first matching or the closest matching exception type.
It seems that catching the first matching is quite simple to implement: the new syntax would be just a syntactic sugar that would expand to the if-conditions as already shown in this topic. On the other hand, catching the closest matching exception type (as in method dispatching) sounds more challenging to implement (?).
With that in mind, catching the first match seems sufficient. I believe that the typical try would come with just a few catch blocks. I would even say that one often needs to handle just a single exception type. Moreover, the different catch blocks would be very close to each other (in terms of both the location in the source code and the program logic), which is not the case when dispatching methods. Overall, I don’t think that catching the first matching exception leaves too much burden to the programmer; the programmer just needs to catch the more specific types first (IF the types even share a parent).
My main takeaway from this thread is that we should not implement the multiple catch block syntax as initially proposed because it is ambiguous. It is unclear if we want to do matching or dispatch.
We literally just implemented dispatching in this thread. It was not too difficult.
You can use begin or let blocks for multiline expressions.
e::AssertionError -> begin
# first lone
# second line
# third line
end
That said we probably should allow for the other anonymous function forms as well.
I would like to add some additional support to this proposal.
There are a few reasons to implement this.
This syntax would allow for a removal of 1 additional level of indentation. (No indentation level required for an extra if construct to check the error type.) This is not a triviality - it makes code easier to read.
It results in a more concise syntax. In my experience less typing, or less verbose languages, result in higher levels of productivity. This is simply because the expressiveness of the language is greater and it takes less time to write the code. Again, not a triviality.
Most other languages support it and therefore if Julia supported such a syntax it would be more closely aligned with what most developers expect to write. It doesn’t necessarily matter if Julia differs from other languages, but in this particular case the additional required if statement is a bit awkward or jarring compared to other languages which have the more succinct form.
However, I also recognized that there may be higher priority tasks or lower hanging fruit for the language developers.
Now I understand the point trying to be made here I realize that imo this makes no sense.
The claim here is that because some authors of Python code use of exceptions wherever possible (something which is already bad design practice) and because specifically in the case ofPython there is no performance penalty for the use of exceptions, therefore Julia should not support them.
This doesn’t apply to any other language, other than Python.
To say Julia shouldn’t support something that most other languages support because some Python programmers might bring their bad habits with them really doesn’t make a lot of sense imo.