Generating functions through an interface (QML)

Hi all, I ran into the world age problem. What I’m trying to do is to make an interface to a physics simulation (classical spin ising) where the interaction function can be edited by the user. As an example, the interaction strength between particles in the model is normally some 1/r like function. I want the end user to be able to edit this function mid-simulation to any function of r. I’m currently using the QML package. I would think that the only real way to do this is by letting the user insert functions (with already specified variable names) as strings and parse them using Meta.parse to a Julia function. Implementing this naively gives me the world age problem I mentioned.

Of course I know that many here like to point out that using metaprogramming is discouraged, so if someone knows another, better way to do something like this I’d be happy to hear it. If not, how do I parse a string and assign it to a function on the fly. I would also like to store a few functions (also entered by the user) in an array of a struct, so that I can call them on the fly later.

Edit: Using invoke latest works, but this adds a lot of overhead. Is there anyway to counteract this overhead? I’ll provide a minimal example later.

If you do something like this:

module M

ftmp = identity

struct Runnable
    x::Vector{Float64}
    y::Vector{Float64}
end

function (r::Runnable)(n)
    r.y .= ftmp.(r.x)
end

function setfn(exstr)
    Core.eval(M,Meta.parse("ftmp = $(exstr)"))
end

end

where Runnable is a stand-in for your app, there’s no world-age issue and the indirection is amortized over a broadcast. Here the user interface takes a string like “f(r) = 1/r” and passes it to setfn.

Last I looked, QML did not handle exceptions well, so you should probably add some sanity checks.

2 Likes

Thanks a lot! This actually works very well. I’m filling an adjacency list according to this function. If I do it with an in code defined function I get around 1.66 ms for a 50x50 lattice. Doing it with your solution it takes 1.86ms, whereas using invokelatest makes the algorithm take 43 seconds!

I don’t understand how your solution works, though. Care to explain?

We’re rebinding the variable ftmp to the new function; when it’s dereferenced the history doesn’t matter. (A purist might use a RefValue{Function} field in Runnable instead of the global.)
Broadcasting acts like a function barrier so the compiler can work without impediment.
(This is my current understanding; I’ll defer to correction by compiler experts.)

Sorry, I still don’t understand. I’m not familiar with the techniques you’re using. I don’t understand why you have to define the struct Runnable, and why r becomes an object of this type as well as a function. I also don’t understand how the broadcasting works here. What is exactly broadcast? And when is the function of r exactly begin run?

Also, do you know why it is still slower than defining the function in code? I want to use it in a different place as well where it will be used in a loop many times, so there the performance difference still matters.