New syntax suggestion: catch with type specs

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

  1. Locate the catch block, ex.args[3]
  2. Find all the statements with a -> head and turn them info different methods of the same function.
  3. Pass e to that function so we can use multiple dispatch.

Here’s the prototype.

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)

Should I turn this into a package?

8 Likes

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
2 Likes

This latter form would essentially replicate Match.jl or Rematch.jl behavior:

try
    ...
catch e
    @match e begin
        e::AssertionError => println("AssertionError")
        e::InexactError => println("InexactError")
    end
end

So I guess it would be superfluous in this form.

1 Like

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.

1 Like

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.

1 Like

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
2 Likes

The Fortran syntax is also not that bad:

select case (n)
  case (-1)
    print *, 'negative one'
  case (0)
    print *, 'zero'
  case (1)
    print *, 'positive one'
  case default
    print *, 'other value'
end select

Common Lisp has case and typecase which might be nicer when matching against types (as in this case):

(typecase error
     (simple-error "not so bad")
     (serious-condition "must leave now")
     (t "giving up on default))

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?

4 Likes

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.

My 2 cents, anyway.

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).

I published my implementation as a library on GitHub: GitHub - dhanak/InlineDispatch.jl: A simple Julia module to perform dispatch on a value of an expression using the `@dispatch` macro.

3 Likes

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.

…and InlineDispatch is now in the General registry. Happy dispatching!

4 Likes

I would like to add some additional support to this proposal.

There are a few reasons to implement this.

  1. 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.
  2. 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.
  3. 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.

1 Like

I don’t understand this - can someone explain with a short example code? I haven’t understood the point being made here.

27 posts were split to a new topic: Use exceptions vs other patterns?

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 of Python 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.

1 Like