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])