Optional macro invocation

What’s the idiomatic and DRY way of writing something like this:

function foo(async::Bool) 
  if async 
    @async dostuff()
  else 
    dostuff()
end

The problem here is that the code is not DRY, I have to call dostuff() twice (and the problem is, in general, there will be more logic duplicated).

I tried referencing the macro but it errors out (syntax error) - in the line of:

function foo(async::Bool) 
  doasync = async ? @async : @sync # errors out
  doasync dostuff()
end 

Any idea? It’s a pretty common pattern for me, for example, I have many instances where if a debug flag is set, certain computations are prefixed with @time. Now it’s the same non-DRY mess.

Thanks!

Does this help ?

macro maybeasync(asyncflag, expr)
    quote
        if $(esc(asyncflag))
            @async $(esc(expr))
        else
            $(esc(expr))
        end
    end
end

function docos(flag)
     @maybeasync flag cos(1)
end
julia> docos(true)
Task (done) @0x00007f49b7e4a4a0

julia> docos(false)
0.5403023058681398

EDIT: There’s probably a cheaper way to do it, in terms of code generation.

Yes, I was leaning towards a macro myself.

If this is the only way to go, I guess it should be something at one level of abstraction higher, to pass the macro, the flag, and the expression (so that it will work with @async but also with @time and others).

And again, I guess one of the issues will be that macros can’t be passed around (referenced) as arguments (or at least I don’t know how).

Yeah. I thought the same thing about a more general macro. This is probably already implemented in a few utility files scattered around github…

macro maybemacro(asyncflag, themacro, expr)
    mccall = Expr(:macrocall, Symbol("@", themacro), LineNumberNode(@__LINE__), expr)
    quote
        if $(esc(asyncflag))
            $mccall
        else
            $(esc(expr))
        end
    end
end
julia> @maybemacro true async cos(1)
Task (done) @0x00007f115df46980

julia> @maybemacro false async cos(1)
0.5403023058681398

julia> @maybemacro true time cos(1)
  0.000002 seconds (5 allocations: 176 bytes)
0.5403023058681398

julia> @maybemacro false time cos(1)
0.5403023058681398

This is where it’d be nice to have first-class macros like first-class functions.

While others have given nice solutions with macros, frankly I would just use a closure, possibly with some syntactic help like

doit(f, ::Val{true}) = @async f()
doit(f, ::Val{false}) = f()

doit(async_flag) do 
    # stuff goes here
end

Thanks so much, @jlapeyre and @Tamas_Papp - I’ll give the two approaches a try today, to see how they work.

They both look great although in the general case I believe that the macro approach could be more efficient as it won’t evaluate the input on every function call. Similar to Metaprogramming · The Julia Language

@Mason first-class macros would be awesome. It looks like lisp-style macros could be “upgraded” to first-class macros. I’d love to see that in Julia.

I may be missing something, but if you branch (either via methods or an if) then it should be called only once.

Sorry, I don’t think I properly articulated what was going through my head.

I have these global (environment) variables which are set upon application initialisation and don’t change throughout the lifetime of the script. Things like DEBUG = true | false, ENV = dev | test | prod, etc. These affect whether or not subsequent macros like @time, @async, etc are used (ie if ENV == dev then many statements are prefixed by the @time macro; but in prod they are not).

I was thinking that with macros I can remove the IF evaluation altogether and just use the corresponding branch throughout the lifetime of the script. So the IF/ELSE is evaluated at parse time and the resulting expression is used from that point on. Similar to the parse time vs run time example here: Metaprogramming · The Julia Language

If you use a global const, the compiler should just eliminate the dead branches for you costlessly. Especially if you dispatch on type.

Constant propagation seems to be already enough for an inlineable function, as long as the global is a const:

doit(f, bool) = bool ? f() : f() + 22
const async_flag = true
function test()
  doit(()-> 22, async_flag)
end
@code_warntype test()
Body::Int64
163 1 ─     return 22 

Interesting - wondering how does that work, given that Julia allows changing the value of the const? I presume the function will be recompiled when that happens?

Oh, yes, that’s a good point. They’re not actually constants but fields in a config object. I could se tup some key constants, like const DEBUG = config.debug. Or maybe just refactor the config type to be immutable - that should have a similar effect as the constants, I expect.

No, that’s exactly why you get a warning when redefining a const - because you need to manually recompile a function to see any new const!

I am not sure (though it may not be “undefined” and just happens to work below).

julia> VERSION
v"1.2.0-DEV.28"

julia> const A = 1
1

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

julia> const A = 2
WARNING: redefining constant A
2

julia> f()
2

No:

julia> const A = 1
1

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

julia> f()
1

julia> const A = 2
WARNING: redefining constant A
2

julia> f()
1

as f only gets compiled when called.

That’s very interesting. Looks like a closure and quacks like a closure, doesn’t it?

here’s a more efficient, in terms of code generation, macro to optionally apply another macro:

macro maybethread(flag, loop)
    if flag
        return quote Threads.@threads $loop; end
    else
        return quote $loop; end
    end
  end

thanks so much @jlapeyre for your example above.

@Tamas_Papp i did not see a way to optionally thread a loop with closures. would be curious about such an alternative solution if you have one.

EDIT: i realize now a huge limitation here is that flag has to be literally false or true and can NOT be a boolean variable :frowning:

I was actually recently trying to figure out this exact case of making @threads optional, based on a flag passed to a function. Would the macro you’re proposing be applicable for something like the following?

function func(args...; use_threads=false)
    @maybethread use_threads for i = 1:10
         do_something_with(i, args...)
    end
end

If not, is there a macro that would work for this kind of use case?

i don’t know how to pass in a variable without duplicating the loop code-gen. so instead, i’m now testing an alternative where a global use_threads is defined.

in my case, the loop is actually bit slower if i haven’t started julia with the -t flag, so i also check for that and skip the @threads macro if there is just one thread.

macro maybethread(loop)
  if use_threads && Threads.nthreads()>1
    return quote Threads.@threads $loop; end
  else
    return quote $loop; end
  end
end