Using @sprintf inside a macro

Since temp_str is not escaped you do not need the let. Local variables get renamed by default, and since temp_str is assigned to and not explicitly marked as global it is a local variable.

https://docs.julialang.org/en/v1/manual/metaprogramming/#Hygiene-1

1 Like

Nice. I have to read more about metaprogramming!

I would do it like this

macro myprintf(x, y...)
    :(print(now(),":",@sprintf($x, $(esc(y))...)))
end

having an extra variabl name is unecessary, and my preferred style is to not assign a variable unless it is needed more than once

@chakravala, thanks! In fact, this is exactly how I implemented it now in production code, for all the reasons you mentioned. Initially, I put a variable just to be more explicit where the problem is. Now it is of course time to clean up.

1 Like

Actually looks like I still run into a problem as above solution is not working in some very common scenario. Namely passing a variable into the macro (passing a number directly works)

import Printf.@sprintf; 
import Dates.now;
macro myprintf(x, y...)
    :(print(now(),":",@sprintf($x, $(esc(y))...)))
end

@myprintf("working %.f", 0.42)

#below fails with ERROR: MethodError: no method matching isfinite(::Symbol)
result  = 0.43
@myprintf("failing %.f", result)
1 Like

Splatting doesn’t quite work like this in macro calls. Try:

macro myprintf(x, y...)
    :(print(now(), ":", @sprintf($x, $(esc.(y)...))))
end

instead. You need to do the splatting inside the interpolation, for y to be spliced correctly into the expression.

Edit: I don’t think you even need the esc.

2 Likes

Thanks @simeonschaub! that indeed works as expected.

I wish somebody with a deep knowledge of macros would have written a bit deeper on the topic with tips and tricks, going into tricky situations like those

Yet, one more problem in this solution. using this macro inside a function seems to be not working

import Printf.@sprintf; 
import Dates.now;
macro myprintf(x, y...)
    :(print(now(), ":", @sprintf($x, $(esc.(y)...))))
end

function t()
    r = 21
    @myprintf("failing %.f\n", r * 2)
end

#calling function t is failing with unknown r
t()

I came up with this solution:

julia> macro myprintf(x,y...)
       :(print(now(),":",@sprintf($x,($(y...),)...)))
       end

after reading this post

But I don’t know why it works :rofl:

Does it work for you, if you just leave out the esc in the above example? This might be a macro expansion issue in Julia.

This is explained in the docs:

https://docs.julialang.org/en/v1/manual/metaprogramming/#Hygiene-1

Julia’s macro expander solves these problems in the following way. First, variables within a macro result are classified as either local or global. A variable is considered local if it is assigned to (and not declared global), declared local, or used as a function argument name. Otherwise, it is considered global. Local variables are then renamed to be unique (using the gensym function, which generates new symbols), and global variables are resolved within the macro definition environment. Therefore both of the above concerns are handled; the macro’s locals will not conflict with any user variables, and time and println will refer to the Julia Base definitions.

Since those variables are not assigned to they are not considered local to the macro and do not have gensymed variables created for them. Thus they do not need to be escaped.

Edit: Leaving out the escape will cause behavior that is likely to be unexpected if the user feeds code into the macro that includes an assignment operation and user expects that assignment operation to apply to a variable outside of the macro.

Indeed, my code doesn’t use esc, but it does splat the y into a tuple and then splat the tuple into the call… and this works… I’m not sure why it works but just splatting y into the call doesn’t

@dlakelan, your code fails for me with exact same error (unknown r)
i.e. does this work for you (julia 1.5)?

import Printf.@sprintf; 
import Dates.now;
macro myprintf(x,y...)
       :(print(now(),":",@sprintf($x,($(y...),)...)))
end

function t()
    r = 21
    @myprintf("failing %.f\n", r * 2)
end

#calling function t is failing with unknown r
t()

Which version of Julia are you on? Are you in a fresh session? I can’t reproduce this on 1.5.

yes. 1.5, fresh

               _
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.5.0 (2020-08-01)
 _/ |\__'_|_|_|\__'_|  |  Official https://julialang.org/ release
|__/                   |

julia> import Printf.@sprintf;

julia> import Dates.now;

julia> macro myprintf(x,y...)
              :(print(now(),":",@sprintf($x,($(y...),)...)))
       end
@myprintf (macro with 1 method)

julia>

julia> function t()
           r = 21
           @myprintf("failing %.f\n", r * 2)
       end
t (generic function with 1 method)

julia>

julia> #calling function t is failing with unknown r

julia> t()
ERROR: UndefVarError: r not defined
Stacktrace:
 [1] macro expansion at D:\buildbot\worker\package_win64\build\usr\share\julia\stdlib\v1.5\Printf\src\Printf.jl:1230 [inlined]
 [2] t() at .\REPL[4]:3
 [3] top-level scope at REPL[6]:1

Yes, that won’t work, because it doesn’t use esc. I meant that one:

Does this one not work for you?

1 Like

Doesn’t esc mean that the symbol y will be used instead of the value of the macro argument y. I’m confused

yes, sorry about that. My fault. Now it all works. Thank you very much
Fingers crossed this covers all the scenarios now.

No, by default, all symbols in macros will be resolved in the scope where the macro is defined and all variables a macro assigns to get a randomized name as to not leak any definitions into the scope the macro is called in. If you actually want to reference variables in the scope the macro is called from or want to be able to define new variables in the macro call’s current scope, you need to wrap the symbol in esc. This leaves these symbols alone during macro expansion. You can also wrap whole expressions in esc, but you need to be careful, because you don’t want to accidentally leak variables, or reference functions from the caller’s scope, when you actually want to call a function defined in the scope the macro is defined in. This can lead to subtle bugs, which is why it’s not the default. You might want to read the section in the docs about macro hygiene to learn more.

1 Like

Yes, I have read the macro hygiene section. I guess it didn’t help me much.

what I understand is that if you define a variable in your macro, it will have it’s name changed to not collide with existing variables in the macro eval context.

what I don’t understand is what esc does. it seems it should prevent the macro system from changing the names of variables. You’re broadcasting the esc across y. so it means you insert the contents of y without any macro hygiene changes?