Understanding Macros – A macro to decorate a Type

I read about macros the last few days and I am starting to love the concept. Due to their powerfulness, I still seem to not be able to get my ideas into an implemented Macro (actually in two cases and this is the first).

Let’s assume I have a decorator pattern like parametrised type like

abstract type P end
immutable struct Q{T <: Float64} <: P
    field::T
end
immutable struct DecoQ{S <: Q{T} where T} <: Q
  base::Q{T}
  info::Int
  DecoQ(t::T) = new(Q(t),0) 
end

get(t::Q{T} where T) = t.field
get(t::DecoQ{S <: Q{T} where T}) = get(t.base)

myF(q::T where T<:Q, i::Int)::Q = Q(i*Q.flield)
function myF(q::T where T <: DecoQ, i::Int)
  info = info+1
  return DecoQ(myF(q.base,i),info)
end

So if I initialise a value to be DecoQ it is a Q decorated with another field and with the getters I have a proper access to the field (I could also write that to access Q instead of it’s field, it’s to “unpeel” the decorator(s)).

With this pattern (adapted from object oriented, I’d be open for a more Julia-type decorator), I would like to have a @macro that easily starts the procedure decorated, i.e. if I have

q::Q{Float64} = Q(0.1337)
qnew = myF(q,23)

I would like to be able to change that last line to

qnew = @decorateQ myF(q,23) 

That changes the call to myF( DecoQ(q,23), 23) for example (or even calculate on i for example changing it to myF( DecoQ(q,46),23) ).
So I can describe what I would like to have, but for the last days I still haven’t understood macros enough to know how to do that. Especially hooking into arguments and local variables is something I haven’t understood yet – so maybe if somebody could solve this, I’d learn more about macros (or give me a hint where to find such ideas).

edit: the first <: P was mistyped to be <: E, Float was corrected to Float64.

You can think of macros as functions that take syntax and return syntax. These “functions” are also not run during runtime but during a special time in the pipeline called “macro expansion”.

So the input syntax you have is:

myF(q,23) 

We can expose the internals of the input syntax with dump:

julia> dump(:(myF(q,23)))
Expr
  head: Symbol call
  args: Array{Any}((3,))
    1: Symbol myF
    2: Symbol q
    3: Int64 23
  typ: Any

We can now rewrite this syntax to the desired output syntax with a macro:

macro decorate(expr::Expr)
    # validate input
    @assert expr.head === :call
    @assert length(expr.args) == 3
    
    f, q, i = esc.(expr.args)
    return quote
        $f(DecoQ($q, $i), $i)
    end 
end

To check what it expands to

julia> @macroexpand @decorate myF(q,23)
quote
    myF((Main.DecoQ)(q, 23), 23)
end

In addition, you probably need to fix a couple of minor things:

  1. probably an abstract type E end is missing,
  2. you did not define Float, did you mean Float64?,
  3. you can’t subtype Q, as it is a concrete type,
  4. you are not using the S at all in DecoQ, are you sure you need it?
  5. T is not bound in DecoQ (captured by the where clause, what did you want to do here?

Also, structs are immutable by default.

1 Like

Very nice! Thanks for this very fast answer. That looks easier to implement than I thought!
Just one further question – is it possible to distinguish either the function I get (can I check that it’s called f?) or the type of argument (that the second one is a ::Q?).
I think the second one would be easier, because if I have further functions
g(p::Something, f::Q, t::Float) I maybe want to decorate the second argument, not the first,
but even more (hence the type check) if qalready is a ::DecoQ I don’t want to decorate further/again.

Thanks again for that fast answer – but I also see, I have to read a lot more about macros - they really seem to be nice (and to some extend also mean, I suppose).

Thanks for the remarks – the code is trying to mimic a more complicated situation.
With the first two, you are completely right, I corrected E to also be P and Float is Float64
For the third – yes that’s always bothering me, that I can’t build a Hierarchy of concrete types – I would like to soooo much,
but then the super type has to be P for them.

For the fourth – no I am not sure, maybe it’s enough to do DecoQ{T}, because the idea is to just introduce an additional field to Q (as decoration)
I want DecoQto b kind of. So also for the constructor it should probably read DecoQ(t::T) where T = new (Q(t),0).

While this typing is quite nice, I am still getting used to it.

Sorry, I fail to understand this.

If you are using a concrete field type for performance, just define it like this:

struct DecoQ{S <: Q}
  base::S
  info::Int 
end

DecoQ(base::Q) = DecoQ(base, 0)

Otherwise, using an inner constructor with info = 0 in an immutable type is pretty meaningless, since you won’t be able to change it.

Generally, while macros are a very powerful feature, I would recommend that you try to learn the language first, see if you can implement your ideas without any source transformations (you usually can, parametric types combined with multiple dispatch is very powerful), then when take a look at macros again.

Thanks, you’re right, that decoQ can not be immutable of course.

My general motivation to look at a macro for this is that:
If I start with some value fo decoQ I can make sure it is after that always decoQ. That’s of course possible with parametric types and the very nice feature of multiple dispatch (hence the getters for example).

But If I have a longer notebook somewhere and start computing with my favourite Q types and after a while I notice (somewhere in cell 45) that the following I would like to execute the following decorated, but I am not sure whether it already is and such – the macro is a good and easy access, because I can just add @decorate to that line.

And maybe – while I am getting used to parametric types and multiple dispatch (actually I did Mathematica for quite some time which does multiple dispatch too, just with a different name) I maybe want to also learn about macros. Sorry, If that sometimes then leads to questions that for an expert might look a little dumb.

Edit: Oh I just saw – in my project the decorated value is actually not changing, so it#s only a value available at creating decoQ that I would like to keep track of, but not to change it (even worse - changing it would make the decorator useless) – so for my transcribed example you’re right, but I was maybe just not awake enough this morning.

I did not mean to imply that you should not learn about macros, or that there is any problem with your questions.

Just that Julia can be overwhelming initially, and it is already very powerful without macros. IMO macros should be used when they provide the most convenient alternative, which cannot be implemented without them and you truly need a source transformation. But to see that, one has to be aware of what Julia (w/o macros) can offer. So some experience is valuable.

Also, macros can be difficult to debug, and specifically hygiene (esc) can be tricky.

Of course this question is also a thought about, whether that’s the right way to go. In short, I am convinced, that with the macro I can spare to recreate data (say within a notebook and creation takes an hour) by using a macro to decorate instead of a new initialisation (and computation) of the data.
But maybe I am also wrong – than this thread is just to learn about macros themselves.