Simple metaprogramming exercises/challenges

Could we brainstorm a handful of metaprogramming examples/exercises/challenges?

  • practical or absurd, both welcome
  • with or without solutions, both welcome
  • support explanations welcome

The idea is ‘Learning through Doing’.

Here is an example (Thanks @fcard):

# Write a 'swap_args' macro that reverses operands for simple
#  `<a> <op> <b>` expressions, so ` @swap_args(2/3)` gives 1.5 i.e. 3/2
macro swap_args(e)
    e.args[2:3] = e.args[3:-1:2]   
    e
end
@swap_args(2/3)
1.50

I will write up a compendium over the next few days and link it at the top of the WikiBook metaprogramming page: Introducing Julia/Metaprogramming - Wikibooks, open books for an open world.

(EDIT:) Looks like we’re getting a good mix of difficulty levels!

π

6 Likes

Maybe call that one swap_operands (because swap(a,b) is likely b,a without the operator) .

Another common macro that I often describe to newcomers is @time.

More sophisticated examples of expression transformations may include functions / macros to match expression against pattern or substitute part of expression, e.g.:

expr = :(num ^ 2)
pattern = :(x ^ n)
matchex(expr, pattern)   # ==> Dict(:x => :num, :n => 2)

expr = :(x ^ n)
subs(expr, n=2)   # ==> :(x ^ 2) 

I used these functions extensively in Espresso.jl, but I find them a funny exercise metaprogramming as well.

3 Likes

Chaining macros? A simple one below:

link(a, b) = quote
    let _ = $a
        b
    end
end

macro chain(as...) = reduce(link, as)
2 Likes

Honestly, I don’t understand any examples here!

edit - By that, I mean I just can’t use these examples into Julia REPL and i feel that this thread would be super useful for beginner’s like me.

2 Likes

One possible example is to generate an inlined polynomial evaluation by Horner’s method, given a variable x and a list of coefficients. This is actually implemented in Base (base/math.jl), but makes a good exercise because it is simple and genuinely practical:

# evaluate p[1] + x * (p[2] + x * (....)), i.e. a polynomial via Horner's rule
macro horner(x, p...)
    ex = esc(p[end])
    for i = length(p)-1:-1:1
        ex = :(muladd(t, $ex, $(esc(p[i]))))
    end
    Expr(:block, :(t = $(esc(x))), ex)
end
9 Likes

The way I understand @dfdx example, it is a task: implement the matchex and subs macros?

(imo)
Macro contributors, Balinus and many others need an accompanying example that is easily absorbed and some words about what is happening. A second more recondite example is welcome, too.

1 Like

Yes, exactly. But you may also find ready-to-use solutions here. The point of these functions as an exercise is to learn how to efficiently traverse and transform Julia expressions, which opens the doors to all kinds of fun stuff like automatic differentiation, devectorization or fast math transformation.

1 Like

I had a problem earlier today of transforming

@muladd a1*b1 + a2*b2 + a3*b3 + a4*b4

into a statement with muladd function calls:

muladd(a1,b1,muladd(a2,b2,muladd(a3,b3,a4*b4))))

credit for the neat solution goes to @fcard on Gitter:


macro muladd(ex)
  @assert ex.head == :call
  @assert ex.args != [:+]
  @assert ex.args[1] == :+
  esc(_muladd_meta(ex))
end

function _muladd_meta(ex)
  if length(ex.args) == 2
    return ex.args[2]
  else
    a, b = ex.args[2].args[2:end]
    rest = _muladd_meta(Expr(ex.head, :+, ex.args[3:end]...))
    return :($(Base.muladd)($a, $b, $rest))
  end
end

This is a good little example of a syntactic sugar macro which can be quite useful for performance.

3 Likes

Here’s another implementation of muladd using packages to make things pretty?

using MacroTools
using ChainMap

muladd_(e) = @match e begin
    +(a_*b_) => Expr(:call, :*, a, b)
    +(a_*b_, c__) => muladd_together(a, b, c)
end

muladd_together(a, b, c) = @chain begin
    Expr(:call, :+, c...)
    muladd_
    Expr(:call, Base.muladd, a, b, _)
end

macro muladd(e)
    muladd_(e)
end
1 Like

Once again one of my macros was stricken with MacroTools… A bit of my soul dies each time. (jkjkjk!)
Personally I prefer to use julia’s own abstractions to make things “pretty”:

macro muladd(add)
  esc(to_muladd(add))
end

function to_muladd(add)
  let mul = operands(add)[1]
    if length(operands(add)) == 1
      return mul
    else
      a, b = operands(mul)
      rest = to_muladd(:( +($(operands(add)[2:end]...)) ))
      return :($(Base.muladd)($a, $b, $rest))
    end
  end
end

operator(ex) = ex.args[1]
operands(ex) = ex.args[2:end]

Here is the dumb thing + error handling and tests: https://gist.github.com/fcard/12a49827cc26a197d4d1e75481216176
All valid ways of doing the same thing, of course.

4 Likes

For a simple, but fun exercise, try writing a macro that turns expressions into anonymous functions, using _ as arguments. An example of the macro being used would be:

map(@par(_ * max(_, 2)), 1:4, 4:-1:1)

# 4 6 6 8

Another thing you could try is adding an option of labeling the _ arguments with numbers, making _1 the first argument, _2 the second and so on. You could also add type annotations.

2 Likes

Well hey, if we’re macro-golfing:

macro muladd(ex)
  foldr((a, b) -> :(muladd($(a.args[2:end]...), $b)), ex.args[2:end])
end

As another starting out idea, here’s the simplest thing I can think of that isn’t equivalent to a closure:

(a, b) = (1, 2)
@swap a b
(a, b) == (2, 1)

Should be pretty easy for a beginner to write.

4 Likes

Here’s a harder variation on the muladd challenge: Make it general enough that it can look at any code and transform the inner muladds, keeping everything else the same. Here’s a version using MacroTools; consider me very impressed if anyone can make it cleaner without :slight_smile:

macro muladd(ex)
  MacroTools.prewalk(ex) do ex
    @capture(ex, +(x_ * y_, cs__)) || return ex
    :(Base.muladd($x, $y, $(foldl((a,b)->:($b+$a), cs))))
  end |> esc
end

a = a1*b1 + a2*b2 + a3*b3
b = a4*b4 + f(a*b + c)
# becomes
a = muladd(a1,b1,muladd(a3,b3,a2 * b2))
b = muladd(a4,b4,f(muladd(a,b,c)))
7 Likes
macro muladd(ex)
  esc(to_muladd(ex))
end

function to_muladd(ex)
  is_add_operation(ex) || return ex
  operands = collect(zip(
    to_muladd.((x->x.args[2]).(ex.args[2:end])), 
    to_muladd.((x->x.args[3]).(ex.args[2:end]))))
    
  last_operation = :($(operands[end][1]) * $(operands[end][2]))
            
  foldr(last_operation, operands[1:end-1]) do xs, r
    :($(Base.muladd)($(xs[1]), $(xs[2]), $r))
  end
end
is_add_operation(ex::Expr) = ex.head == :call && !isempty(ex.args) && ex.args[1] == :+
is_add_operation(ex) = false

?
Of couse I could make this more compact, but I don’t think code golf was ever the point of this. It’s not about the golf, Mike, it’s about the love! (I don’t know what that means)
Nice one with the foldl tho, I keep missing the opportunities to use that :stuck_out_tongue:


Edit: I added this new functionality to the gist of my no-fun version , which I still like despite the hardships we encountered together.
Edit2: It has dawned on me that we may have gone off topic a bit, I will see if I can come up with a few simple macros to make up for it.


Mike Innes really is the He-Man to my Skeletor, I keep trying to take over EterniaJulia by introducing my evil verbosity and taking away all the fun, but he keeps foiling my dastardly plans through the power of GrayskullCodeGolfing.

5 Likes

This is really a great example, because of the cool ways it uses interpolations and escaping. It could be really useful for macro learners, I think, with a line-by-line description of the construction and considerations.

2 Likes

Transform a vector of symbols into a symbol of tuples:
tuplify([:a,:b]) == :((a,b))

My solution works, but it feels a bit kludgy:

function tuplify(x)
    a = Expr(:tuple, x)
    a.args = x
    a
end
julia> t = [:a, :b];

julia> :(($(t...),))
:((a, b))
1 Like

Oh that’s much better, thanks!

1 Like