Evaluate a local variable by name

Is there a particularly elegant way to eval a local variable? My use case would be this function:

function A_dipole(which, x, y, z; A₀=19.8517061694#=ų MHz=#)
	i, j = (Symbol(_) for _ in String(which))
	r = √(x^2 + y^2 + z^2)
	return A₀ * (1/r^3 - 3*eval(i)*eval(j)/r^5)
end

where for, e.g. A_dipole(:xy, x, y, z), the eval(i) would evaluate to the value of x and eval(j) would evaluate to the value of y. I can of course define r_i and r_j values like

function A_dipole(which, x, y, z; A₀=19.8517061694#=ų MHz=#)
	i, j = (Symbol(_) for _ in String(which))
	r_i = (i == :x) ? x : (i == :y) ? y : (i == :z) ? z : 0.0
	r_j = (j == :x) ? x : (j == :y) ? y : (j == :z) ? z : 0.0
	r = √(x^2 + y^2 + z^2)
	return A₀ * (1/r^3 - 3*r_i*r_j/r^5)
end

but I was wondering if Julia provided any neat way that I’m not aware of to evaluate local variables more directly.

Maybe something like this is a bit cleaner?

function A_dipole(which, x, y, z)
    v = (; x, y, z)
    i, j = (Symbol(_) for _ in String(which))
    r_i = getproperty(v, i)
    r_j = getproperty(v, j)
    ...
end

If you changed which to be (:x,:y), that would make the string conversion unnecessary as well, but I gather that being able to send in e.g., :xy is the whole point of the exercise.

Edit: some more messing around with named tuples:

coord_nt() = (;
    xy = (:x, :y),
    xz = (:x, :z),
    yx = (:y, :x),
    yz = (:y, :z),
    zx = (:z, :x),
    zy = (:z, :y)
)

function A_dipole(which, x, y, z)
    v = (; x, y, z)
    i, j = getproperty(coord_nt(), which)
    r_i = getproperty(v, i)
    r_j = getproperty(v, j)
    return r_i, r_j
end

This one is at least type stable, so it’s got that going for it.

3 Likes

eval can do a lot more than retrieve variables, and doing any of that in a local scope prevents method optimization. If you want the runtime state of variables in a local scope, then (Base.@locals) makes a Dict{Symbol, Any}. That is obviously not type stable, so leverage any restrictions on type or number like the above.

Your example is symmetric enough that I would normally just reorder arguments to accomplish the same thing, but I imagine you can’t do that in your actual code.

Yes, that’s great! And v[i] instead of getproperty(v, i) works just fine. So I ended up with

function A_dipole(which, x, y, z; A₀=19.8517061694#=ų MHz=#)
	i, j = (Symbol(_) for _ in String(which))
	r_ = (; x, y, z)
	r = @. √(x^2 + y^2 + z^2)
	return @. A₀ * (1 / r^3 - 3 * r_[i] * r_[j] / r^5)
end

That’s augmented with @., since it had to work on vectors as well as scalars, and then allows me to do A_dipole(:xx, df[:, "x (Å)"], df[:, "y (Å)"], df[:, "z (Å)"]) for a dataframe containing some data for C13 lattice sites w.r.t to an NV center in diamond.

If you changed which to be (:x,:y), that would make the string conversion unnecessary as well

Yeah, I was a little miffed at the String-Symbol roundtripping, but according to BenchmarkTools, the version with (:x, :y) isn’t any faster

but I gather that being able to send in e.g., :xy is the whole point of the exercise.

This is just a function I’m writing for myself in a notebook, so this can be whatever I feel like typing. But yeah, A_dipole(:xy, x, y, z) seemed nicer.

That’s technically the answer to my question, even though using a NamedTuple seems by far the more elegant solution.

This is my actual code :wink: . But like I said, this is just a function in a notebook I’m writing for myself, so the aim was probably “elegance” over most other (e.g., performance) concerns. Just a bit of curiosity to learn about Base.@locals (which I wasn’t aware of), and to be reminded of the existence of NamedTuples (which I tend to forget).

Just a minor note: Whenever you write something like this consider using hypot instead. It protects you against over-/underflows and is shorter to type:
r = hypot.(x, y, z)

3 Likes