Перейти к основному содержимому
Версия: 2.0 (WIP)

Конструкции программирования

Извлечение данных, операции над строками и числами и построение результата дополняются конструкциями, делающими JSONata Тьюринг-полным функциональным языком.

Комментарии

Комментарии можно вставлять в выражения разделителями в стиле C: /* комментарий */. Например, ниже комментарии поясняют длинное выражение, строящее текстовый график синуса:

/* Длинным выражениям может понадобиться пояснение */
(
$pi := 3.1415926535897932384626;
/* JSONata не славится поддержкой графики! */
$plot := function($x) {(
$floor := $string ~> $substringBefore(?, '.') ~> $number;
$index := $floor(($x + 1) * 20 + 0.5);
$join([0..$index].('.')) & 'O' & $join([$index..40].('.'))
)};
/* Факториал — произведение целых чисел от 1 до n */
$product := function($a, $b) { $a * $b };
$factorial := function($n) { $n = 0 ? 1 : $reduce([1..$n], $product) };
$sin := function($x){ /* определяем синус через косинус */ $cos($x - $pi/2) };
$cos := function($x){ /* разложение косинуса в ряд Маклорена */
$x > $pi ? $cos($x - 2 * $pi) : $x < -$pi ? $cos($x + 2 * $pi) :
$sum([0..12].($power(-1, $) * $power($x, 2*$) / $factorial(2*$)))
};
[0..24].$sin($*$pi/12).$plot($)
)

Это выражение строит такой результат.

Условная логика

Тернарный оператор (? :)

Конструкция if/then/else записывается так: predicate ? expr1 : expr2. Вычисляется predicate; если его эффективное логическое значение true, вычисляется и возвращается expr1, иначе — expr2.

Account.Order.Product.{ `Product Name`: $.Price > 100 ? "Premium" : "Basic" }

Для демонстрационных данных оно возвращает четыре объекта, где Cloak имеет значение "Premium", а остальные товары — "Basic".

Оператор Elvis / значение по умолчанию (?:)

expr1 ?: expr2 возвращает левую часть, если её эффективное логическое значение истинно, иначе правую. Это сокращение для expr1 ? expr1 : expr2; оно задаёт запасное значение и не заставляет повторять левое выражение.

Account.Order.Product.{
`Product Name`: $.'Product Name',
`Category`: $.Category ?: "Uncategorized"
}

Оператор coalescing (??)

expr1 ?? expr2 возвращает левую часть, если она определена (не undefined), иначе правую. Это сокращение $exists(expr1) ? expr1 : expr2. В отличие от Elvis, он подставляет запасное значение только при отсутствии левой части, но не для 0, false или ''.

Account.Order.{ "OrderID": OrderID, Rating: ($sum(Product.Rating) / $count(Product.Rating)) ?? 0 }

Переменные

Имя, начинающееся с $, — переменная, то есть именованная ссылка на значение любого типа.

Встроенные переменные

  • $ — переменная без имени, обозначающая контекстное значение в текущей точке иерархии входного JSON;
  • $$ — корень входного JSON. Он нужен, когда требуется временно выйти из текущего контекста и перейти по другому пути, например для перекрёстной ссылки или соединения данных;
  • встроенные (native) функции — см. библиотеку функций.

Привязка переменных

Привязка имеет вид $var_name := "value"; затем значение доступно как $var_name. Область видимости ограничена блоком:

Invoice.(
$p := Product.Price;
$q := Product.Quantity;
$p * $q
)

Выражение возвращает цену Product, умноженную на количество в Invoice.

Функции

Функция — значение первого класса, её можно хранить в переменной, как и любой другой тип. Библиотека встроенных функций помещена в переменные глобальной области видимости. Например, $uppercase содержит функцию, которая при вызове со строковым аргументом str возвращает str со всеми символами в верхнем регистре.

Вызов функции

Функцию вызывают, ставя после её ссылки (или определения) скобки с последовательностью аргументов, разделённых запятыми. $uppercase("Hello") возвращает "HELLO"; $substring("hello world", 0, 5)"hello"; $sum([1,2,3]) — число 6.

Определение функции

Анонимная функция определяется как function($l, $w, $h){ $l * $w * $h }; её можно сразу вызвать с (10, 10, 5) и получить 500, либо сохранить:

(
$volume := function($l, $w, $h){ $l * $w * $h };
$volume(10, 10, 5)
)

Сигнатуры функций

Необязательная сигнатура задаёт типы параметров и результата; движок проверит аргументы до вызова и выдаст динамическую ошибку при несовпадении. Формат — строка <params:return>.

Простые типы: b boolean, n number, s string, l null; сложные: a array, o object, f function. Объединения: (sao), (o), u = (bnsl), j = любой JSON-тип (bnsloa), x = любой тип (bnsloaf). Параметризованные типы: a<s> — массив строк, a<x> — массив значений любого типа.

Примеры: $count<a:n>, $append<aa:a>, $sum<a<n>:n>, $reduce<fa<j>:j>. Модификаторы: + — один или больше аргументов ($zip: <a+>), ? — необязательный аргумент ($join: <a<s>s?:s>), - — при отсутствии аргумента использовать контекст ($length: <s-:n>; эквивалентны $length(OrderID) и OrderID.$length()).

Рекурсивные функции

Функция, сохранённая в переменной, может вызвать себя через эту переменную: ($factorial := function($x){ $x <= 1 ? 1 : $x * $factorial($x-1) }; $factorial(4)) возвращает 24. Рекурсивную функцию можно написать и только анонимными функциями, ничего не присваивая переменной, через Y-комбинатор; подробный пример приведён ниже.

Оптимизация хвостового вызова (хвостовая рекурсия)

Каждый рекурсивный вызов добавляет кадр в стек вызовов. При достаточно глубокой рекурсии стек может исчерпаться. В классическом факториале число кладётся в стек, а при разворачивании стека умножается на факториал числа минус один. В такой записи вычислитель JSONata вынужден хранить промежуточные результаты в стеке, поэтому для большого числа произойдёт переполнение.

В функциональном программировании это известная проблема. Решение — переписать функцию так, чтобы стек не был нужен для промежуточного результата. В хвостовой форме умножение выполняется до самовызова, а результат передаётся вторым параметром $acc (аккумулятором):

(
$factorial := function($x){(
$iter := function($x, $acc) { $x <= 1 ? $acc : $iter($x - 1, $x * $acc) };
$iter($x, 1)
)};
$factorial(170)
)

Самовызов здесь — последнее действие функции. Это хвостовой вызов; распознав его, парсер JSONata внутренне переписывает рекурсию в простой цикл. Поэтому функция может работать неограниченно долго без роста стека. Функции с такой формой называют хвостово-рекурсивными.

Функции высшего порядка

Функцию можно передать параметром другой функции или вернуть из функции. Функции, обрабатывающие функции, называют функциями высшего порядка:

(
$twice := function($f) { function($x){ $f($f($x)) } };
$add3 := function($y){ $y + 3 };
$add6 := $twice($add3);
$add6(7)
)

$twice принимает функцию $f и возвращает функцию с параметром $x, которая применяет $f к $x дважды. $add3 сохраняет функцию прибавления трёх; до этого момента обе функции ещё не вызваны. Вызов $twice($add3) возвращает функцию, которая дважды применяет $add3 к своему аргументу, и сохраняется в $add6. Затем $add6(7) прибавляет 3 два раза и возвращает 13.

Функции как замыкания

Лямбда сохраняет снимок окружения — текущий контекст и переменные в области видимости — в момент определения. При последующем вызове используется это сохранённое окружение, а не окружение вызова; это лексическая область видимости, или замыкание.

Account.(
$AccName := function() { $.'Account Name' };
Order[OrderID = 'order104'].Product.{
'Account': $AccName(),
'SKU-' & $string(ProductID): $.'Product Name'
}
)

Когда создаётся $AccName, контекст $ — значение Account. Позднее, при вызове, контекст уже переместился к каждому Product, но тело выполняется в сохранённом окружении Account. Результат: { "Account": "Firefly", "SKU-858383": "Bowler Hat", "SKU-345664": "Cloak" }.

Частичное применение

Замените один или несколько аргументов функции плейсхолдером ?, чтобы создать новую функцию с меньшей арностью:

(
$first5 := $substring(?, 0, 5);
$first5("Hello, World")
)

Результат — "Hello". Частично применённую функцию можно применять частично повторно: $firstN := $substring(?, 0, ?); $first5 := $firstN(?, 5).

Цепочки функций

Цепочки функций используют двумя способами: чтобы не вкладывать много вызовов при последовательной обработке значения и как конструкцию высшего порядка для определения новых функций из существующих.

Цепочка вызовов

Цепочка вызовов value ~> $funcA ~> $funcB равна $funcB($funcA(value)). Например: Customer.Email ~> $substringAfter("@") ~> $substringBefore(".") ~> $uppercase().

Композиция функций

Цепочка функций создаёт новую функцию: $funcC := $funcA ~> $funcB равнозначно $funcC := function($arg) { $funcB($funcA($arg)) }. Так, ($normalize := $uppercase ~> $trim; $normalize(" Some Words ")) возвращает "SOME WORDS".

Функции как значения первого класса

Композицию функций можно объединять с частичным применением, получая компактный синтаксис определения новой функции: ($first5Capitalized := $substring(?, 0, 5) ~> $uppercase(?); $first5Capitalized(Address.City)) возвращает "WINCH".

Продвинутый пример: Y-комбинатор

Этот раздел необязателен: он не улучшит ни здравомыслие, ни умение преобразовывать JSON. Выражение ниже реализует Y-комбинатор и возвращает 720:

λ($f) { λ($x) { $x($x) }( λ($g) { $f( (λ($a) {$g($g)($a)}))})}(λ($f) { λ($n) { $n < 2 ? 1 : $n * $f($n - 1) } })(6)

Греческую лямбду λ можно использовать вместо слова function; если она есть на клавиатуре, это экономит место и радует любителей лямбда-исчисления. Первую часть выражения можно сохранить и применять к другим рекурсивным анонимным функциям, например для ряда Фибоначчи:

(
$Y := λ($f) { λ($x) { $x($x) }( λ($g) { $f( (λ($a) {$g($g)($a)}))})};
[1,2,3,4,5,6,7,8,9] . $Y(λ($f) { λ($n) { $n <= 1 ? $n : $f($n-1) + $f($n-2) } }) ($)
)

Он возвращает [1, 1, 2, 3, 5, 8, 13, 21, 34]. На практике разумнее использовать именованную функцию:

(
$fib := λ($n) { $n <= 1 ? $n : $fib($n-1) + $fib($n-2) };
[1,2,3,4,5,6,7,8,9] . $fib($)
)