Давайте попробуем посмотреть на Sweet.js, компилятор, который реализует гигиенические макросы для JavaScript.
Работает он довольно просто — вы определяете набор шаблонов, по которым выполняется поиск по синтаксическому дереву. При совпадении макрос получает кусок дерева, который ему нужен и тело макроса определяет как этот кусок дерева должен трансформироваться. Далее результат встраивается обратно в дерево и процедура продолжается с того самого места.
Sweet.js оперирует своим собственным форматом синтаксического дерева, почти на уровне токенов, с минимальной структурой. С одной стороны это делает возможным определять довольно экзотические синтаксисы для своих макросов, с другой — делает написание макросов несколько сложнее, как если бы они были определены над стандартным AST JavaScript.
Начнем с простейшего примера, но сначала надо установить Sweet.js:
После этого у нас должна быть доступна утилита
Теперь чтобы получит ES5 совместимый JavaScript код мы должны просто «скормить» это компилятору
Момент, на который стоит обратить внимание, это то, что Sweet.js сгенерировал имена переменных при раскрытии макроса, таким образом исключив возможность конфликта имен. Это и значит, что Sweet.js реализует гигиенические макросы.
Теперь давайте напишем что-нибудь полезное. Как насчет набора макросов для написания тестов в стиле BDD. Начнем с простейших.
В отличие от формы
Посмотрим что у нас получилось
Это уже более полезно, чем
Посмотрим, что ещё полезного мы можем сделать с макросами для написания тестов. Как насчет набора макросов для написания утверждений (assertions)? Так как макросы имеют доступ к самой структуре кода, мы можем это использовать, для того, чтобы писать утверждения с информативными сообщениями о невыполнении утверждений. Заодно посмотрим как Sweet.js позволяет писать инфиксные макросы.
Как это все будет выглядеть? Я предлагаю следующий синтаксис:
При этом, при невыполненном утверждении я хочу видеть информативное сообщение об ошибке, которое не просто будет показывать значения текущих переменных в стиле
Начнем с того, что напишем макрос, который будет получать любое выражение JavaScript и генерировать строку кода, для этого выражения — «как парсинг, только наоборот». Это понадобится нам, чтобы генерировать информативные сообщения об ошибках.
В отличие от предыдущих примеров, этот макрос представляет собой case-макрос. В отличие от rule-макросов, которые мы использовали ранее, case-макросы позволяют использовать всю мощь JavaScript чтобы определять синтаксическую трансформацию.
Я не буду расписывать детально, что делает этот макрос. Но схема такая — мы определяем функцию
Как видим, все работает на ура, за исключением того, что строка получается без пробелов. Написать лучшую версию макроса
Теперь переходим к непосредственному определению синтаксиса для утверждений. Мы будем использовать модуль
Мы использовали конструкцию
Теперь набор утверждений
будет раскрываться в следующий ES5-валидный код
Задача выполнена! Теперь вы можете начать писать свои макросы под свои задачи или определять свой синтаксис для каких-нибудь библиотек или фрэймворков.
Все макросы, которые я определял в этой статье (и даже чуть-чуть больше) доступен на npm и на github:
Для того, чтобы использовать их нужно сначала поставить необходимые пакеты из npm:
И потом компилировать и тестировать код
с помощью следующих команд
На npm доступны также другие библиотеки с макросами. Предлагаю посмотреть например на sparkler, который реализует сравнение с шаблоном (pattern matching) в JavaScript:
Думаю, было интересно. Обо всех замечаниях, пожеланиях пожалуйста в комментарии или, кто стесняется, мне на email.
UPDATE. Забыл сказать, что Sweet.js имеет возможность генерации карт кода (source maps), поэтому сложностей с отладкой (по крайней мере в браузере) быть не должно.
Работает он довольно просто — вы определяете набор шаблонов, по которым выполняется поиск по синтаксическому дереву. При совпадении макрос получает кусок дерева, который ему нужен и тело макроса определяет как этот кусок дерева должен трансформироваться. Далее результат встраивается обратно в дерево и процедура продолжается с того самого места.
Sweet.js оперирует своим собственным форматом синтаксического дерева, почти на уровне токенов, с минимальной структурой. С одной стороны это делает возможным определять довольно экзотические синтаксисы для своих макросов, с другой — делает написание макросов несколько сложнее, как если бы они были определены над стандартным AST JavaScript.
Начнем с простейшего примера, но сначала надо установить Sweet.js:
npm install --global sweet.js
После этого у нас должна быть доступна утилита
sjs
. Давайте напишем макрос, который будет менять местами значения двух переменных, поместим следующий код в файл swap.sjs
:macro swap {
rule { $x , $y } => {
var tmp = $x;
$x = $y;
$y = tmp
}
}
var x = 11;
var y = 12;
swap x, y;
swap y, x;
Теперь чтобы получит ES5 совместимый JavaScript код мы должны просто «скормить» это компилятору
sjs -r ./swap.sjs
var x = 11;
var y = 12;
var tmp = x;
x = y;
y = tmp;
var tmp$2 = y;
y = x;
x = tmp$2;
Момент, на который стоит обратить внимание, это то, что Sweet.js сгенерировал имена переменных при раскрытии макроса, таким образом исключив возможность конфликта имен. Это и значит, что Sweet.js реализует гигиенические макросы.
Теперь давайте напишем что-нибудь полезное. Как насчет набора макросов для написания тестов в стиле BDD. Начнем с простейших.
let describe = macro {
rule { $name:lit { $body ... } } => {
describe($name, function () {
$body ...
});
}
}
let it = macro {
rule { $name:lit { $body ... } } => {
it($name, function () {
$body ...
});
}
}
describe "My functionality" {
it "works!" {
}
}
В отличие от формы
macro name
мы использовали let name = macro
— это сделано для того, чтобы исключить бесконечную рекурсию. Так как describe
и it
макросы возвращают набор токенов с именами, которые совпадают с именами самих макросов, то Sweet.js будет пытаться применить соответствующие макросы ещё и ещё, пока не кончится стэк. Форма let
помогает избежать этого, так как она не создаёт биндинг для имени макроса внутри синтаксиса, который возвращается макросом.Посмотрим что у нас получилось
describe('My functionality', function () {
it('works!', function () {
});
});
Это уже более полезно, чем
swap
-макрос, который мы написали в самом начале — позволяет сэкономить на написании кода и использовать синтаксические конструкции, более близкие к предметной области.Посмотрим, что ещё полезного мы можем сделать с макросами для написания тестов. Как насчет набора макросов для написания утверждений (assertions)? Так как макросы имеют доступ к самой структуре кода, мы можем это использовать, для того, чтобы писать утверждения с информативными сообщениями о невыполнении утверждений. Заодно посмотрим как Sweet.js позволяет писать инфиксные макросы.
Как это все будет выглядеть? Я предлагаю следующий синтаксис:
2 + 2 should == 4
"aabbcc" should contain "bb"
[1, 2] should be truthy
x.y() should throw
При этом, при невыполненном утверждении я хочу видеть информативное сообщение об ошибке, которое не просто будет показывать значения текущих переменных в стиле
undefined has no method x
, но будет выводить какой именно код привел к этому. Например 2 + 2 should == 5
должен привести к сообщению об ошибке 2 + 2 should be equal to 5
.Начнем с того, что напишем макрос, который будет получать любое выражение JavaScript и генерировать строку кода, для этого выражения — «как парсинг, только наоборот». Это понадобится нам, чтобы генерировать информативные сообщения об ошибках.
macro fmt {
case { _ ( $val:expr ) } => {
function fmt(v) {
return v.map(function(x){
return x.token.inner ?
x.token.value[0] + fmt(x.token.inner) + x.token.value[1] :
x.token.value;
}).join('');
}
return [makeValue('`' + fmt(#{$val}) + '`', #{here})];
}
}
В отличие от предыдущих примеров, этот макрос представляет собой case-макрос. В отличие от rule-макросов, которые мы использовали ранее, case-макросы позволяют использовать всю мощь JavaScript чтобы определять синтаксическую трансформацию.
Я не буду расписывать детально, что делает этот макрос. Но схема такая — мы определяем функцию
fmt
которая обходит синтаксическое дерево и генерирует строку кода из него. Потом мы конструируем другое синтаксическое дерево, которое состоит из одного узла-строки и возвращаем его как результат макроса.fmt(1 + 1) // "1+1"
fmt(x.y(1, 2)) // "x.y(1,2)"
Как видим, все работает на ура, за исключением того, что строка получается без пробелов. Написать лучшую версию макроса
fmt
остается в качестве упражнения читателю.Теперь переходим к непосредственному определению синтаксиса для утверждений. Мы будем использовать модуль
assert
из стандартной библиотеки Node.js для самих утверждений и просто определим макросы, которые будут компилироваться в вызовы функций из этого модуля.var assert = require('assert');
macro should {
rule infix { $lhs:expr | == $rhs:expr } => {
assert.deepEqual(
$lhs, $rhs,
fmt($lhs) + " should be equal to " + fmt($rhs));
}
rule infix { $lhs:expr | be truthy } => {
assert.ok(
$lhs,
fmt($lhs) + " should be truthy");
}
rule infix { $lhs:expr | contain $rhs } => {
assert.ok(
$lhs.indexOf($rhs) > -1,
fmt($lhs) + " should contain " + fmt($rhs));
}
rule infix { $lhs:expr | throw } => {
assert.throws(
function() { $lhs },
Error,
fmt($lhs) + " should throw");
}
}
Мы использовали конструкцию
rule infix
для определения инфиксных правил, символ |
в шаблоне показывает где должен находится символ имени макроса, в данном случае should
.Теперь набор утверждений
2 + 2 should == 4
"aabbcc" should contain "bb"
[1, 2] should be truthy
x.y() should throw
будет раскрываться в следующий ES5-валидный код
var assert = require('assert');
assert.deepEqual(2 + 2, 4, '`2+2`' + ' should be equal to ' + '`4`');
assert.ok('aabbcc'.indexOf('bb') > -1, '`aabbcc`' + ' should contain ' + '`bb`');
assert.ok([
1,
2
], '`[1,2]`' + ' should be truthy');
assert.throws(function () {
x.y();
}, Error, '`x.y()`' + ' should throw');
Задача выполнена! Теперь вы можете начать писать свои макросы под свои задачи или определять свой синтаксис для каких-нибудь библиотек или фрэймворков.
Все макросы, которые я определял в этой статье (и даже чуть-чуть больше) доступен на npm и на github:
- sweet-bdd — describe, it, beforeEach, afterEach,… макросы
- sweet-assertions — should макрос
Для того, чтобы использовать их нужно сначала поставить необходимые пакеты из npm:
% npm install --global mocha sweet.js
% npm install sweet-bdd sweet-assertions
И потом компилировать и тестировать код
describe "additions" {
it "works" {
2 + 2 should == 4
}
}
с помощью следующих команд
% sjs -m sweet-bdd -m sweet-assertions ./specs.sjs > specs.js
% mocha specs.js
На npm доступны также другие библиотеки с макросами. Предлагаю посмотреть например на sparkler, который реализует сравнение с шаблоном (pattern matching) в JavaScript:
function myPatterns {
// Match literals
case 42 => 'The meaning of life'
// Tag checking for JS types using Object::toString
case a @ String => 'Hello ' + a
// Array destructuring
case [...front, back] => back.concat(front)
// Object destructuring
case { foo: 'bar', x, 'y' } => x
// Custom extractors
case Email{ user, domain: 'foo.com' } => user
// Rest arguments
case (a, b, ...rest) => rest
// Rest patterns (mapping a pattern over many values)
case [...{ x, y }] => _.zip(x, y)
// Guards
case x @ Number if x > 10 => x
}
Думаю, было интересно. Обо всех замечаниях, пожеланиях пожалуйста в комментарии или, кто стесняется, мне на email.
UPDATE. Забыл сказать, что Sweet.js имеет возможность генерации карт кода (source maps), поэтому сложностей с отладкой (по крайней мере в браузере) быть не должно.