How to warn new users away from metaprogramming

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.

6 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.

15 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.

4 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.)

2 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, https://github.com/JuliaDiffEq/MuladdMacro.jl 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: https://stackoverflow.com/questions/39627029/julia-how-does-inline-work-when-to-use-function-vs-macro

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

I feel fortunate to have recognized early on that metaprogramming was generally above my skill set! Based on the number of questions that get posed to the forum about it, I think it has saved me some headaches.

But I definitely appreciate the efforts of those who have ventured into the pits…

The biggest problems I see with mis-use of metaprogramming are

  • people who fundamentally don’t understand what macros are and when they run, and are trying to use them for runtime calculations or think they can do something other than saving typing. Consequence: frustration and confusion.

  • people who want to pass function arguments as strings or expressions rather than as…functions. Consequence: frustration and confusion about scope, and terrible performance if they get it to work.

  • trying to use lots of generated variables rather than using a container data structure (e.g. a Dict). Consequence: frustration and confusion about scope, and (probably) inflexible and unreadable code.

22 Likes

Not to derail the conversation, but: One could argue that MathOptInterface is the macro-free interface, and JuMP is “just nicer syntax”.

1 Like

On the other side macro are fun and an exciting feature when you come from other languages that don’t have them. That’s something that motivated me to go into Julia early on, and even though I made all the common mistakes with macro it was still fun (and useful) to learn. That’s also maybe why we see all these beginners misusing macros.

4 Likes

Just to clarify, the proposal is about the scenario where macros yield difficult, buggy, inelegant, and slow solutions. It is my impression from discussions on this forum that the new users who get into these are not having fun — quite the opposite.

There is no intention to deny the usefulness of macros, or to de-emphasize their importance in Julia and how they give a comparative advantage to the language.

Also, from this discussion I think that the title of the topic is too broad: other aspects of metaprogramming are not as affected as macros.

4 Likes

It would be a good start. And if documentation can be divided into “Basic”, “Advanced” section and put metaprogramming under the “Advanced” section would help shun away new users from trying it.

Current TOC may make one think that metaprogramming is as easy and essential as Multi-dimensional Arrays and Missing Values

6 Likes

Absolutely. MathOptInterface is the macro-free interface under JuMP.