Metaprogramming: Passing an expression as a variable

Hey guys,

this is my first ever post on here so please don’t mind if I do something wrong.

Currently I am trying to wrap my head around macros and how to use them and what is possible and what is not. So what I want to have is a variable expr storing some expression and having a while loop looping over the statement stored in this variable. So a small example with a macro called @iterate could look like this.

expr = :(i<5)
i=0
@iterate while expr
    i += 1
    println(i)
end
1
2
3
4

So I had much trouble to get this working somehow. With this code I could get it to work, but I am not totally satisfied with this solution.

macro iterate(expr::Expr)
    conditions = eval(quote quote $$(expr.args[1]) end end)
    statements = expr.args[2]
    return esc(quote
            while $conditions
               $statments
            end
            end)
end

expr = :(i<5)
i=0
@iterate while expr
    global i += 1
    println(i)
end

So I do have some questions:

  1. Does the definition of conditions have to be so difficult and somehow ugly?
  2. I do not like the global i +=1. Is there a way to do this without the global?
  3. Is the idea of my implementation the best one or is there an easier way to do it?

Sorry, if this is somehow trivial. I really tried to look at many threads and macro implementations, but somehow got confused on the way and only could come up with this implementation :grinning:.

Thanks for answers in advance and have a nice day :slight_smile:

Fjedor

You’re aware that you can do:

expr = :(i<5)
i=0
@eval while $expr
    global i += 1
    println(i)
end

? Also note that your iterate macro receives the symbol :eval and not what is in the variable. Thus you need to the the eval inside your macro, which is bad style. Very bad some might say.

Most macros are probably for boiler plate like code. If you find yourself writing a lot of repetitive code you probably want a macro. The @time macro is a good example. You write a lot of:

julia> t0 = time_ns(); my_fn(x,y,z); Int(time_ns() - t0)

and you get tired. You write a macro which takes an expression and surrounds it with the timing stuff:

macro t(e)
:(t0 = time_ns(); $e; Int(time_ns() - t0))
end

julia> @t sin(4)
6214

Your macro could be rewritten like this, and have the same effect. This does not solve your concern with global, though.

macro iterate(expr::Expr)
    expr.args[1] = eval(expr.args[1])
    esc(expr)
end

You should however be aware that there are several problems with such a macro (sorry if this was already obvious to you):

  • it is taylored for while loops in that it supposes that you’ll always find your variable name in expr.args[1]. If you call the macro with anything else, then you don’t know what will happen. You could do some pattern matching to analyze the expression that you get, check that it has the expected form, and extract from it what you want.

  • more importantly, your macro works only because it is syntactically correct to put a single token (your expr variable) instead of a test expression (i<5, the contents of expr, which you want to substitute in the code produced by your macro). As an illustration, try to write a macro which would perform the same kind of things, but for a for loop. Something like this won’t work, because the parser gets confused even before it is able to call your macro:

    expr = :(i in 1:5)
    @iterate for expr
         println(i)
    end
    
  • as already mentioned by @mauro3, using eval is sometimes frowned upon. That being said, I’ve sometimes done such things myself, especially when the macro takes several arguments and you want some to be unevaluated (hence the use of a macro in the first place) and others not (as if they were passed to a function). And I don’t know of any way to avoid eval in such cases (but maybe there exist some…)

Yeah, I think using eval inside the body of a macro is almost always a bad idea. In particular, because eval works only at global scope, a macro which uses eval inside its body will either (a) error or (b) just do the wrong thing silently, depending on what global variable happen to be defined at parse time. That’s basically never what you want–it may work at the REPL, but you’re just asking for bugs later on.

@Fjedor what you’re trying to do isn’t possible to do in a safe way with macros. A macro takes in code (specifically parsed expressions), not values, and outputs new code (more parsed expressions). Your hypothetical @iterate macro takes in the code while expr ... end and is supposed to produce code which depends on the actual value of the variable expr. That’s just not how macros work. When you call the macro @f(x), the macro f just receives the symbol :x (a tiny piece of code) and does not receive any kind of information about what value some variable named x might have in some particular scope.

It might be easier to help you come up with a good solution if you can provide more context about the actual problem you want to solve. What’s the fundamental problem you’re trying to solve with this macro?

4 Likes

On a note related both to your second point (global) and the use of eval: because of scoping rules, your “ideal” example (i.e. without global i) does not work in the global scope:

julia> i=0;
julia> while i < 5
           i += 1
           println(i)
       end
ERROR: UndefVarError: i not defined
Stacktrace:
 [1] top-level scope at ./REPL[1]:2 [inlined]
 [2] top-level scope at ./none:0

This would work (without global i) in a local scope, for example in a function. But because of eval only working with the global scope, the macro fails during expansion if it is not used in the global scope:

julia> macro iterate(expr::Expr)
           expr.args[1] = eval(expr.args[1])
           esc(expr)
       end
@iterate (macro with 1 method)

julia> function foo()
           expr = :(i<5)
           i = 0
           @iterate while expr
               i += 1
               println(i)
           end
       end
ERROR: LoadError: UndefVarError: expr not defined
Stacktrace:
 [1] top-level scope
 [2] eval at ./boot.jl:319 [inlined]
 [3] eval(::Symbol) at ./client.jl:389
 [4] @iterate(::LineNumberNode, ::Module, ::Expr) at ./REPL[2]:2
in expression starting at REPL[3]:4
1 Like

I’m guessing you should really be passing functions around rather than expressions. For example:

function doit(f)
    i = 0
    while f(i)
        i += 1
        println(i)
    end
end

doit(i -> i < 5)
2 Likes

Hey guys,

wow thank you for all the nice input.

@ffevotte and @mauro3 :
Ah okay thanks for the input. Now I understand where this confusion comes from. Great! And you are right that eval might not be the best idea.
I have also encounterd the problem with the global i. That is why I changed it and was a little afraid that I might run into problems :smiley:

@rdeits Yeah, I guess I have to dig into scope and stuff once again to get a better feeling and understanding of all the issues that can occur. I will give an example to explain my motivtion at the end of this post.

@stevengj
Hm, this seems to be a solution that might work for me somehow. I will try it out later and will report if this solves my problems. This might be smarter in this way.

So just to give a little bit more insight in my motivation I will give a small example. Since I have an optimisation and algorithmic background to some extent, I wanted to implement a small algorithmic framework just for learning purpose and also wanted to play around with macros. So I thought that I might want to have a structure with an initialization, a while loop with some stopping criterion and some updates in this loop. But I wanted it to be a bit more flexible, so for example the user can tell the framework the stopping criteria like i<10 or abs(x-x_old)<epsilon or a combination of such. So I thought that it might be a cool idea to do this with the @iterate macro as described before, since it would not distort the syntax to much, but would indicate that something different happens there. But maybe this is not the smartest idea as pointed out before.

If you have any ideas how to do this with a macro this would be fantastic. But I will try to do it as @stevengj sugggested. Maybe this works for me. :slight_smile:

Thanks guys and have a nice weekend
Fjedor

I would absolutely avoid using macros for this. I’d either simply pass arguments (could be optional keyword arguments) max_iterations and tolerance to the method, or pass a function like Steven said. Here’s how that might look like for a dummy optimization method:

function optimize(func, x, stopfunc)
    iter = 0
    yold = NaN
    while true
        y = func(x)
        delta = abs(y - yold)
        stopfunc(iter, delta) && return (x, y)
        yold = y
        iter += 1
        x += 1
    end
end

optimize(z -> 1/z, 1, (iter, delta) -> iter > 1000 || delta < 1e-5)

Returns:

(317, 0.0031545741324921135)
4 Likes

Hey guys :slight_smile:
So I had time to test the suggestions of @stevengj and @bennedich with passing functions. This is much easier and works pretty well so thank you for this contribution :slight_smile: But also to all of the others. I guess I have to dig deeper in some of the topics mentioned in this thread. So thanks again and I will mark @bennedich’s post as the solution.

Fjedor

2 Likes