Base.Cartesian

Модуль Cartesian (неэкспортируемый) предоставляет макросы, упрощающие написание многомерных алгоритмов. Чаще всего такие алгоритмы можно написать с помощью простых приемов. Однако в некоторых случаях Base.Cartesian все же полезен или даже необходим.

Принципы использования

Далее приведен простой пример использования:

@nloops 3 i A begin
    s += @nref 3 A i
end

который генерирует следующий код:

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

В целом модуль Cartesian позволяет писать универсальный код, содержащий повторяющиеся элементы, например вложенные циклы в этом примере. В число других применений входят повторение выражений (например, разворачивание цикла) или создание вызовов функций с переменным количеством аргументов без использования конструкции «звезда» (i...).

Основной синтаксис

(Основной) синтаксис макроса @nloops имеет следующий вид:

  • Первым аргументом должно быть целое число (не переменная), задающее количество циклов.

  • Вторым аргументом является символ-префикс, используемый для переменной-итератора. Здесь использовался i, и были сгенерированы переменные i_1, i_2, i_3.

  • Третий аргумент задает диапазон для каждой переменной итератора. Если здесь использовать переменную (символ) , она будет восприниматься как axes(A, dim). Более гибко можно использовать синтаксис выражения анонимной функции , описанный ниже.

  • Последний аргумент является телом цикла. Здесь он появляется между begin...end.

Дополнительные возможности макроса @nloops описаны в разделе справки.

@nref следует аналогичному шаблону, генерируя A[i_1,i_2,i_3] из @nref 3 A i. Обычно принято читать слева направо, поэтому @nloops — это @nloops 3 i A expr (как в for i_2 = axes(A, 2), где i_2 находится слева, а диапазон — справа), а @nref — это @nref 3 A i (как в A[i_1,i_2,i_3], где сначала идет массив).

При разработке кода с помощью Cartesian может выясниться, что отладка становится проще после изучения сгенерированного кода с помощью @macroexpand:

julia> @macroexpand @nref 2 A i
:(A[i_1, i_2])

Указание количества выражений

Первым аргументом для обоих макросов является количество выражений, которое должно быть целым числом. При написании функции, которая будет работать в нескольких измерениях, возможно, вы не станете прибегать к жесткому кодированию. Рекомендуется использовать @generated function. Вот пример:

@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

Естественно, можно также подготовить выражения или выполнить вычисления перед блоком quote.

Выражения анонимных функций как аргументы макросов

Возможно, самой мощной особенностью Cartesian является возможность предоставлять выражения анонимных функций, которые вычисляются во время анализа. Рассмотрим простой пример.

@nexprs 2 j->(i_j = 1)

Макрос @nexprs создает выражения n, следующие шаблону. Этот код сгенерирует следующие операторы:

i_1 = 1
i_2 = 1

В каждом созданном операторе «изолированная» j (переменная анонимной функции) заменяется значениями в диапазоне 1:2. По сути, в модуле Cartesian используется синтаксис в стиле LaTeX. Он позволяет выполнять вычисления по индексу j. Ниже приведен пример вычисления шагов массива:

s_1 = 1
@nexprs 3 j->(s_{j+1} = s_j * size(A, j))

сгенерирует выражения

s_1 = 1
s_2 = s_1 * size(A, 1)
s_3 = s_2 * size(A, 2)
s_4 = s_3 * size(A, 3)

Выражения анонимных функций имеют множество практических применений.

Справка по макросам

# Base.Cartesian.@nloopsMacro

@nloops N itersym rangeexpr bodyexpr
@nloops N itersym rangeexpr preexpr bodyexpr
@nloops N itersym rangeexpr preexpr postexpr bodyexpr

Генерирует N вложенных циклов, используя itersym в качестве префикса для переменных итерации. rangeexpr может быть выражением анонимной функции или простым символом var, тогда диапазон имеет вид axes(var, d) для измерения d.

При необходимости можно указать «предвыражения» и «поствыражения». В теле каждого цикла они выполняются первыми и последними, соответственно. Например:

@nloops 2 i A d -> j_d = min(i_d, 5) begin
    s += @nref 2 A j
end

сгенерирует следующее:

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

Если вам требуется только поствыражение, в качестве предвыражения следует указать nothing. С помощью круглых скобок и точек с запятой можно задавать выражения, состоящие из нескольких операторов.

# Base.Cartesian.@nrefMacro

@nref N A indexexpr

Генерирует выражения вида A[i_1, i_2, ...]. indexexpr может быть либо префиксом символа итерации, либо выражением анонимной функции.

Примеры

julia> @macroexpand Base.Cartesian.@nref 3 A i
:(A[i_1, i_2, i_3])

# Base.Cartesian.@nextractMacro

@nextract N esym isym

Генерирует N переменных esym_1, esym_2, …​, esym_N для извлечения значений из isym. isym может быть либо Symbol, либо выражением анонимной функции.

@nextract 2 x y сгенерирует

x_1 = y[1]
x_2 = y[2]

тогда как @nextract 3 x d->y[2d-1] выдает

x_1 = y[1]
x_2 = y[3]
x_3 = y[5]

# Base.Cartesian.@nexprsMacro

@nexprs N expr

Генерирует N выражений. expr должен быть выражением анонимной функции.

Примеры

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

@ncall N f sym...

Генерирует выражение вызова функции. sym представляет любое количество аргументов функции, последний из которых может быть выражением анонимной функции и расширяется на N аргументов.

Например, @ncall 3 func a генерирует

func(a_1, a_2, a_3)

тогда как @ncall 2 func a b i->c[i] выдает

func(a, b, c[1], c[2])

# Base.Cartesian.@ntupleMacro

@ntuple N expr

Генерирует N-кортеж. @ntuple 2 i сгенерирует (i_1, i_2), а @ntuple 2 k->k+1 сгенерирует (2,3).

# Base.Cartesian.@nallMacro

@nall N expr

Проверяет, получает ли любое из выражений, сгенерированных выражением анонимной функции expr, значение true.

@nall 3 d->(i_d > 1) сгенерирует выражение (i_1 > 1 && i_2 > 1 && i_3 > 1). Это может быть удобно для проверки границ.

# Base.Cartesian.@nanyMacro

@nany N expr

Проверяет, получает ли любое из выражений, сгенерированных выражением анонимной функции expr, значение true.

@nany 3 d->(i_d > 1) сгенерирует выражение (i_1 > 1 || i_2 > 1 || i_3 > 1).

# Base.Cartesian.@nifMacro

@nif N conditionexpr expr
@nif N conditionexpr expr elseexpr

Генерирует последовательность операторов if ... elseif ... else ... end. Например:

@nif 3 d->(i_d >= size(A,d)) d->(error("Dimension ", d, " too big")) d->println("All OK")

сгенерирует следующее:

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