How to reference argument of expression

Hi,

I’m trying to make an alias to an argument of an expression within a macro, and then change it to something else. Something like:

macro foo(exp)
   arg1 = exp.args[1]
   arg1 = 3
end

However, I find that if I do this, arg1 is actually a copy, and not a reference, to exp.args[1]. How can I do this without having to type exp.args[1] all the time?

I think if you use a view it should work.

See: Assignment and mutation - #4 by StefanKarpinski

(As usual, I would encourage you to think twice about doing metaprogramming, especially when you are still learning the basics of Julia.)

3 Likes

Yes, but then arg1 is a zero-dimensional array and you need to mutate its contents with arg1[] = ....

julia> ex = :(3 + 4 * 7)
:(3 + 4 * 7)

julia> ex.args
3-element Vector{Any}:
  :+
 3
  :(4 * 7)

julia> arg = @view ex.args[2]
0-dimensional view(::Vector{Any}, 2) with eltype Any:
3

julia> arg[] = 111
111

julia> ex
:(111 + 4 * 7)

This is a pretty unusual style of Julia programming, however, especially in a metaprogramming context.

So is there a different option?

What is your overall goal here? Why are you using macros in the first place? We need some background to answer intelligently, to avoid falling into the trap of an XY problem.

1 Like

You can compute what you want to assign and then write out exp.args[1] only once as the left hand side of one assignment. Also, I’m curious what languages work the way you were wanting this to work.

1 Like

I think there will be a mismatch between the part of the language you are targeting (meta programming, lisp based) and your approach of programming (imperative programming, with mutable cell popping here and there, suddenly but exactly when?) . Sadly, still not enough people are correctly trained about FP today, one should have a look to those two books before going to deeply in julia core IMO

Included bonus : be able to explain without any culture shock sims and diffs between

@test cdr(1, 2) == cdr((1, 2)) == (2,)
@test cdr([1, 2]) == [2]

I was looking at this tutorial: Introduction to metaprogramming in Julia | Workshop | JuliaCon 2021. Following the things he did, I was just trying stuff out and I wanted to make a macro to allow for negative indexing of vectors ala Python. E.g. I can write @ni vec[-1] to reference the end of a vector etc. In the video, at around the 55:50 mark the guy in the video does something that I thought to be useful for such a macro, because I can directly reference the -1 and just turn it into the appropriate index. However, the guy in the video himself confesses he is a bit surprised that you can not reference these arguments within the code, which is why I decided to ask.

Right, that’s of course also a way to do it. I don’t know about other languages and their approach to meta programming , since this is my first foray into meta programming at all. In my post above I explain that I was following a tutorial which went over this topic and I was trying to follow what was done there.

Thanks for the context. Implementing negative indexing is indeed a nice learning exercise.

Note, however, that this is not a great practical use-case for macros. The reason is that macros only know how expressions look, not what they mean. If you have an expression x[-1], a macro can see that you have a negative index and turn this into x[end], but if you have an expression x[i], a macro doesn’t have access to the value of i or even whether it is a number at all. e.g. it could transform x[i] to x[i < 0 ? (end+i+1) : i], but that would fail if i is not an integer index (e.g. i could be an array).

Instead, if you wanted to fully support negative indexing, probably you would need to implement a new array type, wrapping an ordinary Julia array, with getindex method that checks whether each index is negative and transforms it accordingly. (That would slow down indexing, but so does negative indexing in Python … it’s just that a slight slowdown of scalar indexing in Python doesn’t matter because Python is slow to begin with.)

The point is that this is not about metaprogramming, it’s just about programming—macros are just ordinary Julia functions that happen to be called on parsed expressions. It would be very rare to see a computer language where:

x = a[i]
x = y

changes a[i]. (I can’t think of any off the top of my head.)

For example, here is an @ni macro that works only on literal integer indices, and doesn’t do any mutation of the arguments at all — it just constructs a completely new argument list to return:

macro ni(ex)
    if Meta.isexpr(ex, :ref)
        newargs = map(ex.args[2:end]) do i
            i isa Integer && i < 0 ? :($(:end) - $(-i-1)) : i
        end
        return Expr(:ref, esc(ex.args[1]), esc.(newargs)...)
    else
        return ex
    end
end

You can see what it does using macroexpand:

julia> macroexpand(Main, :(@ni x[3,-2]))
:(x[3, end - 1])
3 Likes

Metaprogramming in Julia is not really any less imperative than the rest of the language — a macro in Julia is basically just an ordinary Julia function that happens to be called at an unusual time (at code-lowering time, right after parsing but before compilation); you can implement it in an imperative style if you want.

Rather, I think this is the usual confusion between assignment and mutation that arises in all imperative languages that use the same = symbol for both.

2 Likes

It would be very rare to see a computer language where: …

Right I see, although this is basically what references do in C++ right?

I do see your point though, and indeed I realise what was done in the video that I mentioned was a bit different from what I was asking. What he tried to do there is

piece = code.args[2]
piece.args[3] = 3

So it was a composite objects where one of the arguments contained more arguments. Here the assignment of piece.args[3] doesn’t change the original code.
This is more akin to something like this

struct foo
   vec::Vector{Any}
end
bar = foo([1,2,3])

v = bar.vec
v[3] = 4

Which of course changes bar.vec to [1,2,4]. You’re completely right however that what I was trying to doo doesn’t work, and I know that that normally doesn’t work, likewise for other languages like Python.

I think I was thrown off by the above example however, and it’s still not clear to me why that doesn’t work.

Can you give a complete, runnable example of this? I don’t want to have to go through the video to find the code you are talking about.

I just can count how many times i strongly disagree with that.
A quick fact : the count of cdr call in julia lowerer, by julia version

The less one can say, is that the less you know cdr is not the more you will understand the evolution of the julia metaprogramming system


That’s not “metaprogramming in Julia”. It’s the Julia parser, which is written in Scheme (which is of course a mainly functional language). “Metaprogramming” here refers to Julia code that is called after the parser is run (e.g. via macros).

In particular, the body of a macro definition is just standard Julia code that happens to be acting on an Expr object. It’s therefore no more or less functional than the rest of the Julia language — you can implement it in a functional style if you want, but you can also write the macro body in an imperative style.

4 Likes

Yes, here it is

code = Meta.parse("j = i^2")

piece = code.args[2] # returns :(i ^ 2)

piece.args[3] # returns 2

piece.args[3] = 3

piece # returns :(i ^ 3)

code # returns :(j = i ^ 2)

code.args[2].args[3] = 3

code # now returns :(j = i ^ 3)

I can’t reproduce:

julia> code = Meta.parse("j = i^2")
:(j = i ^ 2)

julia> piece = code.args[2]
:(i ^ 2)

julia> piece.args[3] = 3
3

julia> code
:(j = i ^ 3)

That’s weird. I got this exactly from the video I mentioned. I didn’t check it myself though. Maybe this was changed in a newer Julia version?

Nope, it does exactly the same thing in Julia 0.3 from 2015:

   _       _ _(_)_     |  A fresh approach to technical computing
  (_)     | (_) (_)    |  Documentation: http://docs.julialang.org
   _ _   _| |_  __ _   |  Type "help()" for help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 0.3.12 (2015-10-26 12:41 UTC)
 _/ |\__'_|_|_|\__'_|  |  Official http://julialang.org/ release
|__/                   |  x86_64-apple-darwin13.4.0

julia> code = Meta.parse("j = i^2")
:(j = i^2)

julia> piece = code.args[2]
:(i^2)

julia> piece.args[3] = 3
3

julia> code
:(j = i^3)

I haven’t watched the video closely, but it looks like @dpsanders was running in a Jupyter notebook — maybe he accidentally evaluated the code cells out of order?