Engee documentation

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 variables i_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

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

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

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

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

@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