Работает он довольно просто — вы определяете набор шаблонов, по которым выполняется поиск по синтаксическому дереву. При совпадении макрос получает кусок дерева, который ему нужен и тело макроса определяет как этот кусок дерева должен трансформироваться. Далее результат встраивается обратно в дерево и процедура продолжается с того самого места.
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.
ссылка на оригинал статьи http://habrahabr.ru/post/210454/
Добавить комментарий