Looking for good macro examples

I still develop my Julia macros by trial and error, and since they are a bit different from Common Lisp macros, I am realizing that I need to learn a different way for idiomatic usage. The principle is the same as in CL, but hygiene is implemented differently, and there are subtle issues (like no equivalent for &body macros, eg one could not reimplement for or if with macros if they weren’t part of the language). Nevertheless, I am convinced that Julia macros are powerful, I just need to learn the right way to use them.

I am wondering if I could get

  1. Recommendations for examples which I could study to learn about good practices and elegant use of macros, eg in some package code that uses macros elegantly,
  2. Pointers for open issues and corner cases which I should watch out for.

I have read the manual, but it is of course just a short introduction of concepts. Feel free to push your own packages/blog posts :slight_smile:

1 Like

Writing a macro is: (1) take AST apart, (2) do something with the pieces, and (3) put modified pieces back together".

Constructing a AST (step 3) is (usually) quite easy with quote-blocks and interpolation. However, deconstructing a AST (step 1) is a quite a tedious exercise in dissecting nested datastructures. Enter MacroTools.jl which gives you a DSL to do this:

I do macro-batics in my packages GitHub - mauro3/SimpleTraits.jl: Simple Traits for Julia (uses MacroTools) and GitHub - mauro3/Parameters.jl: Types with default field values, keyword constructors and (un-)pack macros, but they are probably not the best examples.

Regarding hygiene, have a look at RFC: WIP: Make macro hygiene easier to use and less error-prone by vtjnash · Pull Request #10940 · JuliaLang/julia · GitHub and https://github.com/JuliaLang/julia/pull/6910.

6 Likes

I think it is unwise to recommend MacroTools for non-expert use. It adds an extra layer of inscrutability on top of meta-programming concepts that many people already have trouble understanding.

1 Like

True, although the OP seems to have experience from Lisp.

Also, by the same token: should one recommend to non-experts to construct the AST by piecing the Expr together by hand instead of using quote and interpolation? Answer: Probably, until the novice understands that meta-programming is nothing magical but instead just manipulation of a nested datastructure.

I find MacroTools.jl a promising start: in general it is my impression that destructuring/unification needs more libraries in Julia, especially for metaprogramming.

Until I understand more about Julia’s macros, I am wary of using it because I don’t yet have a good mental model of

  1. how hygiene works in Julia,
  2. how macros compose in Julia, and what happens to hygiene,
  3. macros that write macros in Julia.
    I will undoubtedly learn about 1–2 by reading examples, and making mistakes, but there seems to be very little of 3 at the moment.

See for an example:

Although, I’m not sure I handle hygiene in the most elegant way. (Edit: In fact, I’m pretty sure I don’t)

Writing macros became a lot more pleasant for me when I gave up on hygiene. Just use gensyms, and prefix every function call with $ModuleName.

1 Like

@cstjean: but you still need to esc expressions from the arguments, don’t you? I wish I could turn the whole hygiene mechanism off somehow.

You can, just put the whole quoted expression in esc.

module MM
foo(y) = println("foo was called with $y")
macro foo_fun(name, expr)
    x = gensym()
    esc(quote
        function $name($x)
            $MM.foo($x)
            $expr
        end
    end)
end
end

using MM: @foo_fun
@foo_fun bobby 45
bobby(4)

I haven’t written any macro-writing-macro yet, I’m not sure how that goes. Julia’s quote is a backquote, but you can get normal-quote by writing

y = :u
quote
    $(Expr(:quote, y))
end

It’s equivalent to ', in Common Lisp.

2 Likes

As for &body, you can get something similar through

macro my_for(do_body, list)
    :(foreach($do_body, $list))
end

@my_for([1,2,3]) do x
    println(x+2)
end

do_body is an Expr that you could destructure.

1 Like

You could argue that the MacroTools readme tries to introduce too much too early, but the basic case looks like:

ex = :(foo(x, y) = x*y)

@capture(ex, f_(args__) = body_)

(f, args, body) == (:foo, [:x, :y], :(x*y))

This is perfectly analogous to construction via syntax (quote, :(...)) so I’m not sure why it should be any more inscrutable or complex; if you can understand one you can understand the other. Like any abstraction it can be leaky, but that doesn’t mean you can’t learn in the abstract and get lower-level as necessary.

Of course, I definitely don’t recommend that beginners try to do more advanced stuff like expression-walking straight away.

1 Like

I think @ihnorton’s point is that macros are already beyond beginners’ proper understanding:

They may be able to copy an example and adapt it in their code to the point of not raising errors, but they cannot say if that actually worsens their code instead of improving it. An extra warning shouldn’t harm.