Macro to create different sets of variables depending on the type of the macro argument

Probably the easiest way to explain what I want to achieve is to give a simple example:

x = 1
@def x 

should be the same as

x = 1
a = sin(x)
b = cos(x)

But if x has a different type I want to define a different set of variables, so

x = complex(1.)
@def x

should be the same as

x = complex(1.)
c = real(x)

Is this somehow possible?

No, itā€™s not. When you call a macro, it only has access to the literal parsed code which was passed to it. In your case, that would be the symbol :x. Thereā€™s no information about the type or value of x when the macro is called.

1 Like

Ok this seems to work:

macro def(x)
    esc(:(
        if isa($x, Int)
            a = sin($x)
            b = cos($x)
        elseif isa($x, Complex128)
            c = real($x)
        end
    ))
end

function test1()
    @def 1
    println(a)
    println(b)
end

function test2()
    @def complex(1., 1.)
    println(c)
end

test1()
test2()

Output:

0.8414709848078965
0.5403023058681398
1.0

Itā€™s better to just define a function

f(x::Int) = (sin(x), cos(x))
f(x::Complex128) = real(x)

a, b = f(1)
c = f(complex(1.))

Do you mean like this? But then I have to write all the variables explicitly, which is what I wanted to avoid in the first place.

A better question is: what are you trying to do? Defining different variables depending on the result of conditions sounds like a bad idea. Canā€™t you just call one of two different functions instead, for example?

I didnā€™t want to bother everyone with the whole problem but if you are interested Iā€™m glad to hear your opinion. So for context, Iā€™m currently working on QuantumOptics.jl, which can be used to solve various quantum problems. Usually the first thing one does is to define a basis and a few typical operators, for example a spin 2 particle:

using QuantumOptics
b = SpinBasis(2)
sx = sigmax(b)
sy = sigmay(b)
sz = sigmaz(b)
sp = sigmap(b)
sm = sigmam(b)

or alternatively a mode of the light field:

b = FockBasis(100)
a = destroy(b)
at = create(b)
n = number(b)

Especially for quickly testing some idea it is a bit tedious to repeatedly run through these steps. So I thought that I could provide some macro that does the same thing in one line:

@qodef SpinBasis(2)

and

@qodef FockBasis(100)

Alternatively, which maybe is a better idea, I could provide a different macro for every basis, like

@spin SpinBasis(2)

and

@fock FockBasis(100)

Iā€™m still undecided if I want to provide such a macro at all, because I really donā€™t like defining variables implicitly, but it would sometimes be very convenient.

1 Like

I would avoid implicit returns because I think it leads to a ā€œmacro magic syndromeā€.

I think that in cases where youā€™d be returning a ton of variables, itā€™s good to have a return type. That is just one return that holds all of the information. You can even endow it with certain behaviors to make it easier to work with. For example, if one of the variables youā€™re returning is a matrix, and the others are statistics on how well something converged, you can put a simple array interface on it to act like that matrix and put a plot recipe to have it auto-plot the convergence results. But to me:

using QuantumOptics
b = SpinBasis(2)
sx = sigmax(b)
sy = sigmay(b)
sz = sigmaz(b)
sp = sigmap(b)
sm = sigmam(b)

that looks like it should be one type that builds a second type with that information.

3 Likes

I agree with @ChrisRackauckas that usually when there is a collection of related variables involved, that is crying out to be wrapped in a type.

If there are two related but different collections of variables, that is two different types, probably with methods defined for the same functions on both types (e.g. an iterator over the basis vectors).

2 Likes

Ok, thank you all for your input! You have convinced me that such a magic macro is not a good idea and I have decided to simply not implement this functionality at all. But it was fun playing around with macros for the first time and maybe I will find a better use for them in the future.

Remember that every time there is a macro statement before an expression, the user is completely up to the documentation of that macro. After all, the macro could transform the AST to exactly anything. Therefore, using a macro based API, while possibly offering terse syntax, is like using a DSL. From experience, it can be quite frustrating when you expect that you are writing Julia code, not just Julia syntax.

JuMP is of course a success story when it comes to macro based API but they have really gone ā€œall inā€ and embraced the DSL approach.

1 Like

If I understand you correctly, it seems to me like what you are looking for is something like

ket = FockVacuum(fields)
@op aā€  aā€  bā€  bā€  ket
n = @op aā€  a ket

basis!(:Fock=>:Spin, ket)

this is the sort of thing you can do with macros, however, in this instance, all the macro is doing for you is providing some notational convenience (which is a pretty nice use for them in these types of cases), the underlying code produced by the macro would simply be calling creation and annihilation operators (Julia functions) as in your example above.

If you are looking to apply operations in the appropriate basis, this seems like a pretty good use for multiple dispatch. You can, for example, make the basis a type parameter like so

struct State{T::Symbol} <: HilbertVector
    ...
end

where T āˆˆ [:Fock, :Spin, ...]. You can then write operators in the abstract that act the appropriate way on the appropriate basis. (This surely isnā€™t the best way of doing it, but itā€™s an example.)

In the vein of @kristoffer.carlssonā€™s comment, you can think of quantum mechanics as a ā€œdomain specific languageā€ (itā€™s not, since everything is quantum mechanics :wink:) and use operators to make Julia code fit it naturally.

Again, I only have a vague idea of what you are trying to do, but these are just some nice features that Julia provides you with.

1 Like