How to warn new users away from metaprogramming

Metaprogramming is a great feature of Julia, allowing advanced users to introduce syntactic extensions with macros, and work around code optimization problems using generated functions. However, it is somewhat of an advanced technique with particular pitfalls, and in practice it is needed much less frequently than some new users of Julia realize, possibly because the basic building blocks of Julia are so powerful.

From some discussions on this forum, it appears metaprogramming is considered as the first solution technique for some users in situations when closures, parametric types, multiple dispatch, traits, and near-zero cost abstraction would be much more idiomatic and compiler friendly.

A particularly problematic senario is when users try to construct code from strings, then parse and evaluate it.

I think that we could do more to clarify these things, but it is not clear to me how to do this best, so I would like to solicit suggestions.

I would consider the following:

  1. a warning at the beginning of the metaprogramming chapter in the manual

  2. possibly suggestions of alternative techniques, eg higher order functions. Should the manual include a quick tutorial about higher order functions (easier to maintain it there)?

  3. should the manual warn about using code generation for simple problems as code smell?

  4. would a FAQ entry about constructing ASTs from strings (“just don’t do it”) make sense?

48 Likes

I like the description here of metaprogramming as automated cutting and pasting.

4 Likes

Yes, I think one of those bright orange Warning boxes near the top of the metaprogramming chapter is in order.

I’m sure my understanding of higher order functions is rather incomplete, so I would read such a tutorial with great interest.

Edit:

The three chapters in the Functional Programming section of Hadley’s Advanced R have some pretty good examples of higher order functions.

4 Likes

I think that the two questions are somewhat orthogonal: the key point is that macros are not the first line solution to a lot of problems that some people apparently use them for. But when suggesting this, the next question will be “what should I do then”, and for this HOF are one important item in the toolbox (among other things). So maybe writing up some short HOF tutorial as part of the manual could work.

In contrast to Hadley’s writeup, I would de-emphasize closures with mutable state (in most cases, you really want a mutable struct for that). Also, simply giving some examples of solving problems with closures used as arguments to sort, map, filter could go a long way for Julia users new to HOF, and then maybe mention function factories briefly.

It is also good to keep in mind that closure is kind of an anonymous struct, in a complementary way that a NamedTuple is (one emphasizes field access, the other callability), and while they are great for prototyping some code naturally evolves to struct anyway.

What I am unsure of is if these things belong in the manual, a blog post, or a book on Julia.

7 Likes

Maybe it would help to have a paragraph at the end of the performance tips section listing the things that you don’t need to worry about in julia (but which might avoid because of your experience with another programming language).

For example, if you’re coming from a language where higher order functions work by passing function pointers around, and the only way to get performance is via “templating” or some other metaprogramming mechanism, then your natural instinct would be to do the same in Julia. A simple example showing how a function can be passed as an argument and still be in-lined into the callee might be all that is needed to make people think otherwise.

17 Likes

Why not both all three?

In all seriousness, I think putting a warning in the manual is a great idea as among the most visible ways of pointing to this. A blog post (or a few) showing examples of different approaches or showing domain-specific problems would be great resources to point users to if they need additional help, and that kind of content didn’t make sense I’m the manual. I don’t know what the use case for books are, I’ve never read a programming book, but I’m sure there’s a place for it too.

I’m sort of on the other end though, macros scare me and I actively avoid trying to write them - as such I don’t really understand what they’re good for, other than using the well-created ones that already exist like @test or @benchmark.

3 Likes

They are good for avoid code duplication when you can’t do it with a function.

As someone who’s actually never written anything using metaprogramming (I’ve actively avoided it), I’d like to suggest that it’s not just “new users” who should stay away from it; it’s “all users, unless it’s the only way to reasonably solve your problem”. My reasoning (personal opinion, YMMV, etc.), is as follows:

  1. Metaprogramming introduces complexity outside of the Julia language itself. It has its own conventions, pitfalls, and syntax, and it requires a level of reasoning about the computation path that transcends what’s necessary to understand Julia as a programming language.

  2. Metaprogramming makes sense for some domains, but my feeling is that people (especially new users?) are turning towards MP as the first way of doing something, as opposed to understanding how to do the equivalent in “standard” Julia.

  3. Personally, I stay away from integrating libraries that use metaprogramming (SimpleTraits excepted) because I have a hard time understanding how they work, and how they might fail. This is a personal limitation but I’m throwing it out there because I think it’s important to maintain simplicity in library and user code, and I see metaprogramming as complicating things, sometimes unnecessarily.

7 Likes

I’m sure this is perfectly sensible, I just don’t have any context for what kind of problem would lead to this issue. I have no doubt that it can be useful - the fact that there are bunch of macros in Base is enough to convince me of that, I’ve just never taken the time to try to learn how those problems are different than the ones that don’t use macros. And I basically can’t understand the thing until I try to use it myself, my CS knowledge is entirely ad hoc.

But it’s ok, at this point, I’m content to let others use it and just treat it like black magic :joy:.

1 Like

Metaprogramming in Julia is mostly (uniquely?) code generation, the only usage I know is to create small DSL like it’s done in Base.Test, in these cases I don’t think you should be afraid of it.

Maybe by clarifying typical usages it can prevent some abuse.

Le lun. 24 févr. 2020 à 01:01, Seth Bromberger via JuliaLang julialang@discoursemail.com a écrit :

1 Like

If you don’t have a case like that then just don’t use macros, thats what you are doing and that’s fine.

It’s a rule to determine when you should use macro, not a rule to generate examples.

2 Likes

That is of course valid advice, but I think that newcomers to Julia are affected the most since they are unaware of the powerful alternatives the language offers. Experienced users just learn how to solve a lot of problems without source transformations and for most of them that is sufficient.

4 Likes

I couldn’t agree more (and sorry if it wasn’t clear in my post). In my experience, one of the hallmarks of an experienced Julia programmer is the facility with which s/he can use the core language simply, to express complex ideas.

4 Likes

I think in many ways that metaprogramming is less abstract than alternatives. It’s a solution that leverages a skill we all already have: we know how to write code to solve simple problems, so faced with several programs, we can imagine what the code that solves them looks like. That’s not abstract, that’s familiar. Now it is just a matter of writing some code that writes that code for us.

APIs, interfaces… I can’t speak for others, but they take me a lot more time and iterations before I start to find reasonable solutions. It takes vision.

Earlier today I commented out (will delete soon) a net of about 700 lines of generated functions because I realized how they could be replaced with multiple dispatch using just a handful of lines of code plus an API change.
It wasn’t my desire to cut out hundreds of lines of code that motivated me, but that I was trying to extend functionality and was faced with either adding more alternatives to that growing mass of @generated, or finding a way to get more code reuse. It’s the evolution of replacing hastily written shortsighted code with code benefiting from hindsight.
In much of my own work, @generated is a symptom of the former.

I admire broadcast.jl. Look how much can be accomplished with dispatch!

But there are cases where I don’t see myself being able to get away from it anytime soon, like in LoopVectorization, where metaprogramming lets me move some expensive calculations to compile time.

Maybe if I spent less time playing with @generated, I’d learn how to design APIs and how to “use the core language simply, to express complex ideas” more quickly. I think that in itself is a good reason to recommend new users to focus on these.

17 Likes

Here is an example:
https://github.com/KristofferC/SIMD.jl/blob/8204863834b50e8a38aca76793137d2acd2ff52c/src/LLVM_intrinsics.jl#L160-L193.

Without a loop with @eval in it you would have to copy-paste the function body ~20 times while only changing the function name. More code, error prone for typos and harder to refactor.

I think the problem with metaprogramming (and macros in particular) is that people think they can somehow do something you can’t do in normal code. Almost by definition, they just expand to code you could have written yourself.

2 Likes

A very nice introduction to (Common Lisp) macros can be found in Peter Seibel’s Practical Common Lisp:

http://gigamonkeys.com/book/macros-defining-your-own.html

complete with a just-so-story, discussion of leaks, and how to plug them.

I have been a very heavy user of Common Lisp macros, to the extent that I wrote a macro-heavy library which still ends up in the top 10 Quicklisp downloads.

But I avoid them almost completely in Julia, except for replacing repetitive code in loops with @eval. I think this is because

  1. as emphasized above, the language itself is much more powerful so syntactic transformations are needed much less.

  2. macros do not blend seamlessly into syntax like S-expressions (I am not talking about the @).

  3. Julia has taken a different route to macro hygiene which I find difficult to work with conceptually for complex macros, especially macro-writing macros.

6 Likes

I’m going through a similar process myself. I used to think that generated functions were necessary for a lot of things that I couldn’t do without meta-programming in other languages. Then I realized that Julia’s zealous adherence to the @inline directive makes it possible to do a lot just using recursive function calls on shrinking tuples. I just rewrote some code (a variable number of nested for-loops) that I originally did with generated functions and Base.Cartesian. The non-generated code (using recursion instead) is just as effective and much easier (for me) to read.

Another example: It’s possible to do SMatrix * SMatrix multiplication with up to 30 elements per matrix using only recursive function calls. (Beyond that limit it seems that I need a generated ntuple function, but the matrix multiplication itself can still be done using recursion.)

3 Likes

Correct me if I’m wrong, Julia use metaprogramming as a the solution to add user defined keywords and they’re prepended by @ to distinguish them from language keywords.

So the question is when do you need new keywords ?

My answer is it’s a good solution when you’ve identified a class of objects which have similar properties but can have different code representations.

An exemple of this is markup languages like XML/HTML, elements have a tag name, eventually some typed attributes, and a content made of children elements constrained by type and quantity or text.

If for some reason you want typed elements (differents struct for each type of element for ex.) having a macro that generate elements with these informations is probably cleaner.
To name it, @element is well a new keyword like struct, function…

For @generated, the class of functions is a bit more vague imo.

Le lun. 24 févr. 2020 à 10:12, Tamas Papp via JuliaLang julialang@discoursemail.com a écrit :

A good rule of thumb is that you should know exactly what your macro is converting things to, otherwise the macro has gone too far. For example, GitHub - SciML/MuladdMacro.jl: This package contains a macro for converting expressions to use muladd calls and fused-multiply-add (FMA) operations for high-performance in the SciML scientific machine learning ecosystem is fairly clear what it does: it just finds things like a*b + c and turns it into muladd(a,b,c). These then nest, so a*b + c*d + e*f becomes muladd(muladd(...)..). Do you know how to do it by hand? Yes. Do you want to? No. That’s a good spot for a macro.

Even when making a DSL, I think the “non-DSL” version should be very clear. For example, I wish JuMP had documented their macro-free interface, so the macro was “just nicer syntax”.

12 Likes

I don’t see a strong need to warn folks away from metaprogramming in general. Using a for loop to define lots of functions or methods — while sometimes tricky to get right — isn’t going to lead to major issues for them down the road. It’ll be either obviously right or wrong, and the alternative (manually writing all those definitions yourself) isn’t really “better” in any meaningful sense. If they try to do something at local scope, it most likely won’t work at all.

Where I see folks having trouble, though, is with defining their own macros. Here’s a great example: Julia - how does @inline work? When to use function vs. macro? - Stack Overflow

Macros are powerful, but so are Julia’s functions. It’s in this case that things are error-prone (and non-obviously so, given escaping), and there’s a significantly better alternative that they should be using. Unless you know exactly what you’re doing, you should just use a function. That’s where we should put warnings and red flags for folks.

9 Likes