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 ) 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