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