Macro hygiene with try/catch

I have a macro that is supposed to define a new function in the caller’s scope (plus do several other things). The code to be evaluated is wrapped in a try/catch block so that if something fails, I can clean things up. However, the functions get renamed by the macro expander (hygiene), even though the whole expression is wrapped in esc. MWE:

macro test()
	fun_def = :( foo(x) = 2*x )

	# ( ... modify some global state ... )
	
	return esc(
		quote
			try
				# (... do some stuff that might fail ...)
				$fun_def
			catch e
				# ( ... revert the modification of the global state ... )
				rethrow(e)
			end
		end
	)
end

julia> @test
(::getfield(Main, Symbol("#foo#13"))) (generic function with 1 method)

It has been suggested that this may have something to do with the local scope introduced by try. I have tried various ways to make fun_def escape hygiene, but none of them work. Any suggestions?

try (just like every other expression) has a returned value, in this case the last expression in the try block itself. You can create a variable in the scope where the macro is expanded by assigning the result of the try to a variable:

function outer()
  fun_def = try
    function(a, b, c)
      a + b + c
    end
  catch e
    println("nope!")
  end

  @show fun_def(1, 2, 3)
end

Your macro just needs to generate the fun_def = try ... end code.

2 Likes

Thanks for the suggestion, but it seems no better:

macro test2()
	esc( quote
		func = try
			foo(x) = 2*x
		catch e
			println("nope")
		end
	end )
end

julia> @test2
(::getfield(Main, Symbol("#foo#14"))) (generic function with 1 method)

julia> func
(::getfield(Main, Symbol("#foo#14"))) (generic function with 1 method)

I want foo to exist in the caller space, not a variable that is bound to the function with a garbled name.

Besides esc not seeming to propagate into the try block, I also find it odd that @macroexpand does not show (in either example) that foo gets renamed.

@redits suggested to use an anonymous function. But even without that, you’ll find that func is defined in your example.

julia> macro test2(fn)
               esc( quote
                       const $fn = try
                              x -> 2*x
                       catch e
                               println("nope")
                       end
               end )
       end
@test2 (macro with 2 methods)

julia> @test2(foo)
#19 (generic function with 1 method)

julia> foo
#19 (generic function with 1 method)
1 Like

Ah, I misunderstood the intent. That seems to be an effective workaround. Thanks for the help.

However, if I am honest, I find this workaround unsatisfying. It feels like a hacky way to achieve the clear and obvious intent of the generated code in my first example. (It actually becomes clunkier in my actual use case, in which the macro creates several functions and a new type.) Basically I would like things to work the “normal” way, i.e. like it does when the function is not put in a try block: A function named foo is added to the global function table, accessible by that name (and not some generated name) to the caller and everyone else who wants to use it. The kind of solution I was hoping for was (1) some syntax for getting foo to be escaped, and (2) some explanation of why esc does not escape code inside a try block.

You can use global inside try to define a global function

julia> try
           foo() = 1
       catch
       end
(::var"#foo#94") (generic function with 1 method)

julia> foo
ERROR: UndefVarError: foo not defined

julia> try
           global foo() = 1
       catch
       end
foo (generic function with 1 method)

julia> foo
foo (generic function with 1 method)

By the way, the issue here is unrelated to macro hygiene (which does not happen if you wrap everything with esc). It’s just closure name mangling.

1 Like

Ah, I think that explains everything. I did not realize that functions defined in local scopes (which is evidently what you mean by a closure? I usually think of closures as anonymous functions) get renamed.

Right, closure is probably not the right term here since foo doesn’t capture any local variables. Maybe anonymous function was better but I thought using it for f(x) = ... form (not x -> ... form) could be confusing. Anyway, IIUC they are implemented with the same mechanism.

They don’t get renamed. Anonymous functions and closures don’t get a name [1] in the first place. What you are seeing are the type of them.

This is because, in Julia, you get “global” type for any kind of callable object (though I believe this is an implementation detail). You can see it by doing something like:

julia> module Demo
       a() = f() = 1
       b() = f() = 1
       c() = () -> 1
       end
Main.Demo

julia> names(Demo, all=true)
14-element Array{Symbol,1}:
 Symbol("#3#4")
 Symbol("#a")
 Symbol("#b")
 Symbol("#c")
 Symbol("#eval")
 Symbol("#f#1")
 Symbol("#f#2")
 Symbol("#include")
 :Demo
 :a
 :b
 :c
 :eval
 :include

julia> Demo.a()
(::Main.Demo.var"#f#1") (generic function with 1 method)

julia> typeof(Demo.a())
Main.Demo.var"#f#1"

julia> typeof(Demo.a()) == getfield(Demo, Symbol("#f#1"))
true

julia> Demo.b()
(::Main.Demo.var"#f#2") (generic function with 1 method)

julia> typeof(Demo.a()) == typeof(Demo.b())
false

julia> Demo.c()
#3 (generic function with 1 method)

julia> typeof(Demo.c())
Main.Demo.var"#3#4"

[1]: I think a more precise way to say this is that they don’t get the show method reflecting the variable names they are assigned to.

Ok, let me make sure I understand your demo:
a, b, and c are the names of global functions.
#a, #b, and #c are names of their respective types.
Each occurrence of f() = 1 is an anonymous function. Their types are named #f#1 and #f#2.
() -> 1 is also an anonymous function, of type #3#4.

They don’t get renamed. Anonymous functions and closures don’t get a name [1] in the first place.

My function was renamed in the sense that I explicitly gave it a name foo, but the parser decided to identify it some other way, so that I could not subsequently access it as foo.

In retrospect, this behavior logically arises from the fact that try introduces a local scope (which I did not realize), and functions defined in local scopes are anonymous (which I also did not realize). The first of these facts is documented in the manual, but may be unexpected to people coming from other languages. And I am not aware that the latter fact is addressed in the manual. It seems that defining things in a try block is a fairly natural thing to do, and it might be helpful if the manual had a comment and/or example about this. I would be happy to add this if people think it would be helpful. But if the consensus is more “No, you were doing something kind of weird and you needed to read the manual more carefully” then I will happily move on with greater knowledge than I had a day ago.

Yeah, I think you get it.

What do you mean by “name”? This is to say, what is the "name of g" in:

julia> f() = 1
f (generic function with 1 method)

julia> g = f
f (generic function with 1 method)

I assumed that what you meant by “name” is whatever printed in Julia REPL. That’s why I said:

In your example g doesn’t have a name. But my example was of the form

try
  foo(x) = 2*x
catch;
end

I gave the function the name foo. However, what is returned from this is (::getfield(Main, Symbol("#foo#13"))).

I think you are mixing the way object is printed and name of the variable. I think the correct statement is "foo is not printed as foo“, not "foo does not have a name” or "foo is renamed."

No, it is not simply a matter of how things are displayed:

julia> try
	foo(x) = 2*x
catch;
end
(::getfield(Main, Symbol("#foo#13"))) (generic function with 1 method)

julia> foo
ERROR: UndefVarError: foo not defined

The function I defined is not accessible outside the try block by the provided name foo. Now, your comments indicate you understand foo as a variable, but I think it is not a variable. It is a name (of a function). This is just like when we define abstract type Bar end, Bar is not a variable, but a name (of a type). That is the semantic and also a common way of talking about functions, regardless of how they are implemented internally within Julia.

Terminology quibbles aside, I now understand why my original code didn’t do what I expected and how to get it to work, for which I am appreciative.

That’s nothing to do with the “name.” As you’ve already noticed, try introduces a scope:

julia> x
ERROR: UndefVarError: x not defined

julia> try
           x = 1
       catch
       end
1

julia> x
ERROR: UndefVarError: x not defined

So, any kind of “assignment”, including the function definition, must be explicitly marked as global if you want to access it from the global scope.

I’m not sure what is the origin your misunderstanding. But maybe it helps to notice that foo(...) = ... is not a syntax for “global” function:

julia> function f(x)
           g() = x
           g
       end
f (generic function with 1 method)

julia> g
ERROR: UndefVarError: g not defined

julia> f(1)
(::var"#g#1"{Int64}) (generic function with 1 method)

julia> f(1)()
1

I don’t think I’m misunderstanding anything. I agree with everything you said in the preceding post, except your comment that “it has nothing to do with name”. By definition, a scope is a region of code in which a variable (i.e. name) is visible. Inside the local scope where the function is defined, it is accessible by the name foo. That name is not valid outside that local scope, as indicated later in that section of the manual: “local scopes are not namespaces, thus variables in an inner scope cannot be retrieved from the parent scope through some sort of qualified access.”

We evidently have a different understanding of some things and seem unable to convince each other of the correctness of our respective points of view. So I don’t think it is productive to continue this particular discussion. I thank you for providing a solution to my original post, and will now bow out of this conversation.

1 Like