How best to write this macro?

I’m learning about meta programming and as practice, I tried to define a macro that would batch define and initialize arrays of a certain type. I got some of the way, but it’s clunky. What would be a better way of writing this macro that would work for the second call, also, without replacing n with $n? (I know that there are better ways of initializing a bunch of arrays, but that won’t help me learn about macros).

macro zeros( tp, list... )
  str = ""
	for el in list
      els = split(strip(string(el),['(',')']), [','])
      str *= string( strip(els[1]),"= zeros($tp," )
      for j = 2:length(els) str *= strip(els[j]) * ( j<length(els) ? "," : ");" )  end
  end
  eval(Meta.parse(str))
end

function ugh()
    @zeros Float64 (x,10,3) (y,4)
    n = 10
    @zeros Int64 (x,n,3) (y,4)
end

ugh()

If you are using this as a learning platform, it is OK, but I don’t see the reason for using a macro.

function zeros_batch(type, args)
    for arg in args
        zeros(type, arg...)
    end
end

zeros_batch(Float64, [[10, 3], [4]])

You should avoid making the expression using strings (unless there is no other way). You need to use Expr, quote end, etc for making your expressions.

1 Like

Is there a reason you’re trying to do this with strings? I would strongly recommend taking advantage of Julia’s built in code representation, Expr. Furthermore, you should almost never need to use eval within a macro.

I think you would benefit a lot from reading https://docs.julialang.org/en/v1/manual/metaprogramming/index.html which I think is an excellent introduction to julia metaprogramming.

2 Likes

Thanks @Mason. I did read that and I agree that it’s an excellent introduction, but clearly not enough for me.

Ah, I see, I can try and wrangle up a few more references to send you if you would like!

In the meantime, here is how I would write your macro:

macro zeros(tp, list...)
    defs = map(list) do ex
        @assert ex.head == :tuple
        name = ex.args[1]
        size = ex.args[2:end]
        :($name = $zeros($tp, $name, $(size...)))
    end
    esc(Expr(:block, defs...))
end

We can look at the code it expands to with @macroexpand to make sure it does what we want:

julia> @macroexpand @zeros Float64 (x, 10, 3) (y,4)
quote
    x = (zeros)(Float64, x, 10, 3)
    y = (zeros)(Float64, y, 4)
end
4 Likes

That would be awesome, thanks!

A bit of explanation, list is going to be Tuple of expressions which I expect are of the form :((name, size...)), so I am going to map over that tuple to turn it into the desired form.

The function map(f, v) takes a function f and applies it to each element of a container v and returns the result, e.g.

julia> map(x -> x + 1, 1:4)
4-element Array{Int64,1}:
 2
 3
 4
 5

The syntax

map(v) do x
    #= stuff here =#
end

is just a fancy way to write

map(x -> #= stuff here =#, v)

so,

julia> map(1:4) do x
           x + 1
       end
4-element Array{Int64,1}:
 2
 3
 4
 5

Alright, so now we see what I’m doing when I write

    defs = map(list) do ex
        @assert ex.head == :tuple
        name = ex.args[1]
        size = ex.args[2:end]
        :($name = $zeros($tp, $name, $(size...)))
    end

This is mapping over list, asserting that each element of that list is an expression whose head is :tuple (i.e. it’s something of the form :((x, y, z))), and then taking out the first element of that tuple and saying that’s going to be our variable name, and the rest will say how big our array should be. With those pulled out, I create a new expression that :($name = $zeros($tp, $name, $(size...))) where I have interpolated in the variables name, tp and size into a expression an array definition to name. I also interpolated in the function zeros to ensure that the function I call zeros gets used, not whatever some other user might have bound to zeros.

This gives us and array of expressions for array definitions and I then stick them into an Expr(:block) which is the construct julia uses for holding multiple expressions together, e.g.

julia> ex = quote
           1 + 1
           2 + 3
       end
quote
    1 + 1
    2 + 3
end

julia> dump(ex)
Expr
  head: Symbol block
  args: Array{Any}((4,))
    1: Expr
      head: Symbol call
      args: Array{Any}((3,))
        1: Symbol +
        2: Int64 1
        3: Int64 1
    2: Expr
      head: Symbol call
      args: Array{Any}((3,))
        1: Symbol +
        2: Int64 2
        3: Int64 3

I then apply esc to that function which is related to the macro hygeine stuff in this section of the docs.

This escaped expression is then returned, not eval'd, so that it functions like a normal macro.

4 Likes

This recent discourse thread has some great resources for learning metaprogramming,

2 Likes

This is really helpful @Mason. Thanks a bunch!

1 Like