Simple metaprogramming exercises/challenges

The @horner macro is also cool because it would be so hard to do in a C-like macro system – you really need a full programming language in your macro system.

1 Like

Here is a challenge: Evaluate a function on a box in d dimensional space. Where d is not fixed.
E.g:-

> @evalongrids println 1:2 1:2 1:2
111
211
.
.

My answer is:

julia> macro evalongrids(fn, grids...)
           variables = [gensym() for g in grids]
           code = :($fn($(variables...)))
           for (v, g) in zip(variables, grids)
             code = :(for $v in $g
                                $code
                           end)
           end
           code
       end
julia> @evalongrids println 1:2 1:2 1:2
111
211
.
.

I guess this behaviour can be emulated using itertools.product in python3.

1 Like

Yeah, that needn’t be done with macros — note that you’ll have some hygiene issues if you use that macro outside of a shared global scope.

We also have Iterators.product.

2 Likes

Showing the macro version first, then the generated function and then the product iterator version is still kind of cool since it mirrors the development history of the language itself and each transition accordingly has a nice “ah, that is better” effect.

9 Likes

Agree @StefanKarpinski .
And most languages do the above using recursion. In fact, Iterators.product also uses recursion.
In Julia, we can directly write code that will generate variable number of for loops! Which is cool.
I would love to see the generated function version of it.

Sorry to chime in, and I know Matt recently gave a similar answer, but is there a more fleshed out post on a concrete example of this evolution and design reflection somewhere? Tracking issues is a bit tricky given the local picture of Julia it can give at the time of development.

1 Like

What if you want a more complicated loop such as

for i1 = 1:N-2, i2 = i1+1:N-1, i3 = i2+1:N 
end 

(with hopefully obvious generalisations)

At the moment I am doing it with @generated, which works fine. I feel I could write an iterator, but it is not obvious that it would be simpler or more performant?

This tutorial notebook might be helpful: https://github.com/stevengj/18S096/blob/master/lectures/lecture7/Metaprogramming.ipynb

Problem set 2 from that seminar has a continued-fraction metaprogramming problem: https://github.com/stevengj/18S096/tree/iap2017/pset2

5 Likes

Here’s one I had some trouble with:
Create a function mapbody that applies a function f to the body of a function expression functionExpr.

My solution:

function mapbody(f,functionExpr)
    ans = deepcopy(functionExpr)
    ans.args[2] = f(ans.args[2])
    ans
end

Are you familiar of GitHub - FluxML/MacroTools.jl: MacroTools provides a library of tools for working with Julia code and expressions.? I don’t have my own user experience but I have seen it recommended multiple times. Macros fascinates me and I wish I could find a time to learn to use them.

Yep, that was my original approach but this is much simpler. I often forget going from first principles is even an option :slight_smile:

Here’s one using recursive @generated functions that I can’t imagine doing in another language:

Aim: A generalised algorithm to walk nested trees of structs and tuples and convert them to and from flat structures.

Conditions:

  1. Field handling methods are outside of pure functions so they can be changed by users.
  2. Fields may return an empty return value.

General solution:

# Build a list of expressions wrapped in splats
nested(T::Type, t, expr_builder, handler=default_handler) =  
    handler(T, [Expr(:..., expr_builder(T, t, fn)) for fn in fieldnames(T)])
# Default to wrapping the list of expressions in a tuple
default_handler(T, expressions) = Expr(:tuple, expressions...)

An implementation for flattening arbitrary nested structs to a tuple, with a method for types in Number:

# Expression builder that returns a call to the generated function
flatten_expr(T, t, fn) = :(flatten(getfield($t, $(QuoteNode(fn))))
# The actual generated function just calls nested()
@generated flatten(t) = nested(T, :t, flatten_expr) 
# Method overrides. wrap results in something splattable.
flatten(x::Number) = (x,)

You could also use a handler that constructed types to run the process in the opposite direction.

Its hardly long enough to be a package anymore, but:

1 Like