Macro that construct function names and call them

My actual usage is that I have a bunch of (~30) functions that performs similar but different operations depending on one argument.

The most straight way to implement is:

function do!(arg1, l)
    if l == 3
        # do something
    elseif l == 5
        # do something
    # ~30 elseifs
    else
        throw(ArgumentError("Invalid l: $(l)")
    end
end

Another way is to implement a bunch of functions do_n! and dispatch them:

const FUNC_DICT = Dict(
    3 => do_3!,
    5 => do_5!,
    # and so on
)

function do_wrapper!(arg1, l)
    if !(l in keys(FUNC_DICT))
        throw(ArgumentError("Invalid l: $(l)")
    end
    FUNC_DICT[l](arg1)
end

I wonder whether I can use a macro to perform such dispatch, and I’m found a way:

macro dis_do(arg1, l)
    return quote Expr(:call, Symbol("do_", $(esc(l)), "!"), $(esc(arg1))) end
end

Then I can run eval(@dis_do(arg1, 3))
My question is, can I modify the macro so that the eval is not needed?

Yes and no. With

macro dis_do(arg1, l)
    return Expr(:call, Symbol("do_", l, "!"), (esc(arg1)))
end

you can do @dis_do(arg1, 3) but it only works with a literal second argument since macros cannot see argument values (there are no values at the time of macro expansion). I.e., you cannot do

l = 3
@dis_do(arg1, l)

Yet another option would be to dispatch on Vals:

function do!(arg1, ::Val{3})
    # (do_3!)
end

function do!(arg1, ::Val{5})
    # (do_5!)
end

...

function do!(arg1, l::Integer)  # Specify type of l to avoid infinite recursion on e.g. do!(arg1, 4)
    return do!(arg1, Val(l))
end

do!(..., 3)  # equivalent to do_3! call
l = 5
do!(..., l)  # equivalent to do_5! call
do!(..., 4)  # ERROR: MethodError: no method matching do!(::..., ::Val{4})

It’s hard to say which approach would be best, though. In particular, what’s the issue with the original do! (elseif), or do_wrapper! approaches?

1 Like

Generally, I would lean towards the Dict approach for its simplicity and extensibility. But there is a lot that can shift that decision.

In most cases an occasional dynamic dispatch is a negligible cost. But if it isn’t (e.g., this function is called many times and the functions it calls are short) and and your functions all share identical input and output types then a tool like FunctionWrappers.jl that can make the dictionary approach above avoid dynamic dispatch. Then it ought to make it perform similarly to the elseif chain. Though if the dispatch cost was already negligible then there was nothing to improve.

If you use the dict approach without FunctionWrappers then you might benefit from type-annotating the return value to avoid a type instability cascade. For example, FUNC_DICT[l](arg1)::typeof(arg1) if the output type is the same as the input type.

1 Like

I would not advice that solution, if all the functions are similar but have minor differences, the best thing to do would be to write a major function and then inside call the other functions to which the argument is passed. I think that creating so many different functions, especially inside a macro and the call them, can really be a nightmare in terms of performance, readability, maintainability and debug

I’m just lazy and think the macro approach might require the minimum typing

No, functions are not created by the macro, it is only a convenient way to get rid of the lengthy else-ifs. The only difference in the code is to replace each do!(arg1, l) with eval(@dis_do!(arg1, l)), and everything else is the same. If some do_x! function throws an error, in the macro approach the error is also shown clearly.
That being said, I suppose there will be little difference in readability, maintainability and debug. But I’m not sure whether eval will have a great impact on run-time performance.