Base.Cartesian
The Cartesian (non-exportable) module provides macros that simplify the writing of multidimensional algorithms. Most often, such algorithms can be written using https://julialang.org/blog/2016/02/iteration [simple techniques]. However, in some cases, Base.Cartesian
is still useful or even necessary.
Principles of use
The following is a simple usage example:
@nloops 3 i A begin
s += @nref 3 A i
end
which generates the following code:
for i_3 = axes(A, 3)
for i_2 = axes(A, 2)
for i_1 = axes(A, 1)
s += A[i_1, i_2, i_3]
end
end
end
In general, the Cartesian module allows you to write universal code containing repetitive elements, such as nested loops in this example. Other uses include repeating expressions (for example, unrolling a loop) or creating function calls with a variable number of arguments without using the "star" construction (i...
).
Basic syntax
The (basic) syntax of the macro `@nloops' looks like this:
-
The first argument must be an integer (not a variable) specifying the number of cycles.
-
The second argument is the prefix symbol used for the iterator variable.
i
was used here, and the variablesi_1, i_2, i_3
were generated. -
The third argument specifies the range for each iterator variable. If you use a variable (symbol) here, it will be perceived as `axes(A, dim)'. More flexibly, you can use the syntax for expressing an anonymous function described below.
-
The last argument is the body of the loop. Here it appears between
begin...end
.
Additional features of the `@nloops' macro are described in in the help section.
'@nref` follows a similar pattern, generating A[i_1,i_2,i_3]
from @nref 3 A i'. It is usually read from left to right, so `@nloops
is @nloops 3 i A expr
(as in for i_2 = axes(A, 2)
, where i_2
is on the left and the range is on the right), and @nref
is '@nref 3 A i` (as in A[i_1,i_2,i_3]
, where the array comes first).
When developing code using Cartesian, it may turn out that debugging becomes easier after examining the generated code using @macroexpand
:
julia> @macroexpand @nref 2 A i
:(A[i_1, i_2])
Specifying the number of expressions
The first argument for both macros is the number of expressions, which must be an integer. When writing a function that will work in multiple dimensions, you probably won’t resort to hard coding. It is recommended to use `@generated function'. Here is an example:
@generated function mysum(A::Array{T,N}) where {T,N}
quote
s = zero(T)
@nloops $N i A begin
s += @nref $N A i
end
s
end
end
Naturally, you can also prepare expressions or perform calculations before the 'quote` block.
Anonymous function expressions as macro arguments
Perhaps the most powerful feature of Cartesian is the ability to provide expressions for anonymous functions that are evaluated during analysis. Let’s take a simple example.
@nexprs 2 j->(i_j = 1)
The macro @nexprs' creates expressions `n
following the pattern. This code will generate the following statements:
i_1 = 1
i_2 = 1
In each created statement, the "isolated" j
(an anonymous function variable) is replaced with values in the range 1:2'. In fact, the Cartesian module uses LaTeX-style syntax. It allows you to perform calculations based on the index `j
. Below is an example of calculating the steps of an array:
s_1 = 1
@nexprs 3 j->(s_{j+1} = s_j * size(A, j))
generates expressions
s_1 = 1
s_2 = s_1 * size(A, 1)
s_3 = s_2 * size(A, 2)
s_4 = s_3 * size(A, 3)
Anonymous function expressions have many practical uses.
Help for macros
#
Base.Cartesian.@nloops
— Macro
@nloops N itersym rangeexpr bodyexpr
@nloops N itersym rangeexpr preexpr bodyexpr
@nloops N itersym rangeexpr preexpr postexpr bodyexpr
Generates N
nested loops using itersym
as a prefix for iteration variables. rangeexpr
can be an expression of an anonymous function or a simple symbol var
, then the range has the form axes(var, d)
for measuring `d'.
If necessary, you can specify "pre-expressions" and "post-expressions". In the body of each cycle, they are executed first and last, respectively. For example:
@nloops 2 i A d -> j_d = min(i_d, 5) begin s += @nref 2 A j end
generates the following:
for i_2 = axes(A, 2) j_2 = min(i_2, 5) for i_1 = axes(A, 1) j_1 = min(i_1, 5) s += A[j_1, j_2] end end
If you only need a post-expression, you should specify as a pre-expression nothing
. Using parentheses and semicolons, you can specify expressions consisting of several operators.
#
Base.Cartesian.@nref
— Macro
@nref N A indexexpr
Generates expressions like A[i_1, i_2, ...]
. indexexpr
can be either a prefix of the iteration symbol or an expression of an anonymous function.
Examples
julia> @macroexpand Base.Cartesian.@nref 3 A i
:(A[i_1, i_2, i_3])
#
Base.Cartesian.@nextract
— Macro
@nextract N esym isym
Generates N
variables esym_1
, esym_2
, …, esym_N
to extract values from isym'. `isym
can be either a Symbol
or an expression of an anonymous function.
'@nextract 2 x y` will generate
x_1 = y[1] x_2 = y[2]
whereas @nextract 3 x d->y[2d-1]
outputs
x_1 = y[1] x_2 = y[3] x_3 = y[5]
#
Base.Cartesian.@nexprs
— Macro
@nexprs N expr
Generates N
expressions. expr
must be an expression of an anonymous function.
Examples
julia> @macroexpand Base.Cartesian.@nexprs 4 i -> y[i] = A[i+j]
quote
y[1] = A[1 + j]
y[2] = A[2 + j]
y[3] = A[3 + j]
y[4] = A[4 + j]
end
#
Base.Cartesian.@ncall
— Macro
@ncall N f sym...
Generates a function call expression. sym
represents any number of arguments to a function, the last of which can be an expression of an anonymous function and is expanded into N
arguments.
For example, @ncall 3 func a
generates
func(a_1, a_2, a_3)
whereas @ncall 2 func a b i->c[i]
outputs
func(a, b, c[1], c[2])
#
Base.Cartesian.@ncallkw
— Macro
@ncallkw N f kw sym...
Generates a function call expression with named arguments kw...
. As in the case of @ncall
, sym
represents any number of function arguments, the last of which can be an expression of an anonymous function and is expanded into N
arguments.
Examples
julia> using Base.Cartesian
julia> f(x...; a, b = 1, c = 2, d = 3) = +(x..., a, b, c, d);
julia> x_1, x_2 = (-1, -2); b = 0; kw = (c = 0, d = 0);
julia> @ncallkw 2 f (; a = 0, b, kw...) x
-3
#
Base.Cartesian.@ntuple
— Macro
@ntuple N expr
Generates a tuple of N
elements. '@ntuple 2 i` will generate (i_1, i_2)
, and @ntuple 2 k->k+1
— (2,3)
.
#
Base.Cartesian.@nall
— Macro
@nall N expr
Checks whether any of the expressions generated by the expression of the anonymous function expr
gets the value `true'.
@nall 3 d->(i_d > 1)
generates the expression (i_1 > 1 && i_2 > 1 && i_3 > 1)
. This can be useful for checking boundaries.
#
Base.Cartesian.@nany
— Macro
@nany N expr
Checks whether any of the expressions generated by the expression of the anonymous function expr
gets the value `true'.
@nany 3 d->(i_d > 1)
generates the expression (i_1 > 1 || i_2 > 1 || i_3 > 1)
.
#
Base.Cartesian.@nif
— Macro
@nif N conditionexpr expr
@nif N conditionexpr expr elseexpr
Generates a sequence of operators if ... elseif ... else ... end
. For example:
@nif 3 d->(i_d >= size(A,d)) d->(error("Dimension ", d, " too big")) d->println("All OK")
generates the following:
if i_1 > size(A, 1) error("Dimension ", 1, " too big") elseif i_2 > size(A, 2) error("Dimension ", 2, " too big") else println("All OK") end