[Julia usage / metaprogramming] How to evaluate a module's function with function's name as a string?

Hi!
Inside a function, I would like to evaluate a module’s function based on two inputs: 1) the module itself and 2) the function’s name as a string. Here is the code:

module A
function foo()
    println("This is A.foo")
end
end

function bar(m::Module, s::String)
    # evaluate "m.s"
end

bar(A, "foo")

I have several questions:

  1. How can I evaluate "m.s" inside the bar function to produce the expected output?
julia> bar(A, "foo")
This is A.foo
  1. Do you have suggestions about better practices to achieve what I would like to do? I explain a bit more: the bar function will belong to a package that will handle some user-defined modules (in our example: A would be defined by a user). At end, a user would have to code module A and to write the functions inside (such as foo) in a config file. This config file design choice makes it easier to pass the function names as strings. While I am confident about the fact that I shall pass the function’s name as input, I do not know if it is a good thing to pass a whole module as input.

Thanks in advance for your help!

Simple answer to your question:

function bar(m, s)
    m.eval(Meta.parse(s))()
end

In answer to your broader question about whether this is a good approach in general, I would
ask why the package that bar belongs to needs to interact with modules at all. If bar
can be passed a module object, why can’t it just directly be passed foo?

2 Likes

If bar is only intended to run a function with a given name (and maybe arguments), you could just use eval, where you can use the

I think that what you want to do is served by the eval function; each module has its own version of it. Using your example:

module A
function foo()
    println("This is A.foo")
end
end

Then:

julia> A.eval(:(foo()))
This is A.foo

Or if what you have is the name of the function (as a string or symbol), and maybe you need to pass some arguments:

module B
function foo(x)
    println("This is B.foo with argument ", x)
end
end
julia> x = 1
1

julia> fname = :foo
:foo

julia> expr = :($fname($x))
:(foo(1))

julia> B.eval(expr)
This is B.foo with argument 1
1 Like

This is related to some design choice: from the user’s point of view, we have to provide 1) the A module containing some functions such as foo and 2) a config file listing the functions inside A that will be used during the global execution. To make it clearer, say we have foo1 and foo2 inside A. The user could then be willing to use bar with maybe only foo1 during the global execution. Later, during another run, the user would be using foo1 and foo2. Truth is that I will be passing the list of used functions as strings and all this could be achieved simply with the config file.

I would avoid relying on eval for this, instead using getfield or getproperty on the module object:

julia> module A
       foo() = "Hello"
       end
Main.A

julia> function bar(mod, name)
           fun = getfield(mod, name)
           fun()
       end
bar (generic function with 1 method)

# Passing the name directly as a Symbol
julia> bar(A, :foo)
"Hello"

# Or converting the String to a Symbol
julia> bar(A, Symbol("foo"))
"Hello"
9 Likes

This can be done with getfield rather than eval.
Which is a bit nicer, since it is much more limitted.

function Bar(mod::Module, s::String)
    return getfield(mod, Symbol(s))()
end

The functions within a module are it’s fields.
getfield takes a Symbol rather than a String but you can construct a Symbol from a String

You can also do things like checking if the function name is one that is within the module, using fieldnames(mod).

Edit: ninja’ed by @ffevotte

3 Likes

Thanks! What is the advantage of getfield over eval?
I noticed allocation and computational improvements:

module A
function foo()
    println("This is A.foo")
end
end

function bar1(m::Module, s::String)
    m.eval(Meta.parse(s))()
end

function bar2(m::Module, s::String)
    getfield(m, Symbol(s))()
end

s = "foo"
@btime bar1(A, s)  # 28.833 μs (20 allocations: 608 bytes)
@btime bar2(A, s)  # 8.229 μs (6 allocations: 144 bytes)

eval can evaluate any Julia code and as such can do almost anything. Which makes it harder for the compiler to reason about your code: it cannot assume anything. And it has to compile things.

On the other hand, getfield makes it easier to reason about your code: all that is needed is to look up some function name in a namespace. In particular there is no need to compile anything new.

4 Likes

I’m not sure I understand the relationship between foo1 and foo2 here… Are they different implementations of the same function? Or two sub-steps within foo?

In other words, what does it mean that bar can use foo1 alone or foo2 alone or (foo1 and foo2)?

Functions in A such as foo1 and foo2 are meant to be different. To simplify, say we would like to execute them sequentially. This makes a comprehensive example I believe:

module A
function foo1()
    println("Foo 1")
end
function foo2()
    println("Foo 2")
end
end

function bar(m::Module, l::Array{String})
    for s in l
        getfield(m, Symbol(s))()
    end
end

Two different usages depending on what functions of A the user wants to execute:

julia> bar(A, ["foo1"])
Foo 1

julia> bar(A, ["foo1", "foo2"])
Foo 1
Foo 2

And the reason I do not directly pass an array containing the functions A.foo1 and A.foo2 themselves is that I want the user to specify the function names inside a configuration file. The user-defined module A is something fixed but depending on the case, one would want to specify different functions of A in the configuration file.

It looks like you’re using modules mostly like dictionaries mapping names to functions. So to answer your broader question from the OP, your current architecture seems to do the job.

2 Likes