Все ниже написанное соответствует Haxe 2 и почти верно для Haxe 3. Но работать без изменений будет только во 2-м. Кто адаптирует все примеры на 3-й и поделится с остальными, получит много экспириенса. Так же написанное не претендует на истину в последней инстанции и может содержать ошибки.
Хочу немного рассказать о макросах в Haxe, статья будет именно вводная, попробую рассказать и показать зачем вообще нужны макросы и какие задачи они могут решать в повседневной жизни.
Макросы в Haxe используют неоправданно мало программистов, кого-то отпугивает их синтаксис, кто-то просто не знает, что это и зачем. Все возможности макросов отлично описаны в статье метапрограммирование на википедии. В двух словах скажу: макросы позволяют как модифицировать существующий код (самомодификация кода), так и создавать новый (генерация кода). В этой статье мы попробуем генерировать новый код, но в очень ограниченных масштабах. Такие вещи, как генерация классов или enum-ов или редактирование существующего кода, я не затрону, лишь замечу, что все это возможно в пределах Haxe макросов!
Так что же на самом деле макрос в Haxe? Вы не поверите, но макросы это обычные функции. Ну не совсем обычные, но и ничего незнакомого по сравнению с кодом Haxe в них нет. Главные плюсы макросов - это то, что в них, кроме стандартной библиотеки, доступны еще и все классы пакетов neko и haxe.macro и то, что функции-макросы вызываются на этапе компиляции, а не во время выполнения программы и возвращают Haxe код, который и будет выполнен на этапе исполнения программы. Neko дает доступ к файловой системе и вообще к системе в целом, а haxe.macro классы позволяют... Да они все позволяют: создавать новые классы, менять структуру существующих, получать полные данные о всех типах, enum-ах и т.д. в общем полный доступ. Главное знать, что именно вам надо. Все это и делает макросы такими интересными для нас. Но не будем сильно затягивать и посмотрим на пример, который будет запоминать в скомпилированном приложении и выводить дату сборки проекта (без сторонних утилит такую задачу решить довольно сложно и тут на помощь приходит макрос):
Функция getBuildDate и есть наш макрос. Сразу обращаем внимание на мета тег @:macro в начале функции - это отличительный знак, говорящий компилятору, что выполнять эту функцию надо во время компиляции! Наша функция возвращает тип Expr, о нем немного позже. Дальше в методе getBuildDate создается обычный Date с текущей датой, который переводится в строку и передается в “магический” Context.makeExpr и еще менее понятный метод Context.currentPos. И тут стоит вернуться к типу Expr, его определение легко найти в коде модуля haxe.macro.Expr
Expr - это просто структура с двумя полями: expr - любое выражение, допустимое в Haxe, но не в привычном виде, а как одно из значений ExprDef enum-а (перечисляемого типа), например EFunction, EVar, ENew, EConst, ECall, EContinue, EReturn и т.д. И pos - указатель на место, где в файле будет расположено выражение. На самом деле pos самому вычислять придется не так часто и для начала запомним, что на его место подставляем Context.currentPos(). Вот как выглядит Position тип:
Теперь, когда Expr перестал быть полной загадкой, вернемся к методу Context.makeExpr. Из названия понятно, что он что-то делает и вот тут лучше, как говорится, один раз увидеть, что-же он делает:
Вернитесь к описанию структуры Expr и убедитесь, что перед нами действительно Expr, содержащий одну строковую константу Econst(CString()) с датой сборки проекта и расположенную в Main.hx на 8 строке, и занимающую 8-20 символы, и это очень важно! Взгляните на 8-ю строку первого примера с макросом, там написано:
8-й символ - как раз начало getBuildDate строки (“trace(“ + 2 символа табуляции в начале строки), а 20-й - ее окончание. Т.о. вызов макроса заменится на выражение со строковой константой “2013-05-04 14:11:44”. Если вы упустили этот волнительный момент, я повторюсь: Мы только что заменили вызов метода на строку с текущей датой, и эта замена выполнена во время компиляции один раз!
Если запустить теперь приложение, мы увидим Main.hx:8: 2013-05-04 14:11:44. И сколько бы раз мы не запускали приложение, дата на экране не будет меняться, т.к. она была записана на этапе компиляции.
Вот и свершилось: вы разобрали и, надеюсь, поняли свой, возможно, первый, но далеко не последний макрос. Хотя уверен, что остался вопрос: “Что за магические #if !macro перед main функцией?”. Все дело в том, что macro функции сильно влияют на поведение класса, в котором они определены, да и импорты, доступные макро функциям, часто непозволительны остальному коду, вот и пришлось так извиваться, чтобы уместить все в один модуль. Но мой вам совет (да и вообще так правильнее): пишите макросы в отдельно отведенных для этого классах, не смешивая их с остальным кодом. Дальше я буду делать только так, но “смешанный” вариант я не мог не показать.
Признаюсь, в этой статье я хотел написать еще кучу всяких макросов, чтобы завлечь как можно больше людей, но понял, что ваши знания очень малы, и делать что-то крутое слишком рано. А вот усложнить наш первый макрос - самое время!
Усложним задачу. Использовать строку с датой хорошо, и ее можно распарсить в привычный Date, ну так давайте и сразу вызывать парсинг строки, т.е. чтобы getBuildDate() заменялся на Date.fromString(“2013-05-04 14:11:44”); Обратите внимание, что макро функции могут вернуть целое выражение.
Задачу мы себе поставили не самую простую, а даже немного сложную. makeExpr нам уже не поможет, он умеет работать только с базовыми и перечисляемыми типами (Int, Float, String, Bool, Array и анонимные объекты, составленные из этих типов), самое время обратиться к Context.parse методу. Смотрим, что у меня получилось:
Видно, что Context.parse первым параметром принял строку, содержащую Haxe код, а вот на ее результат советую взглянуть самим, лишь подскажу, как я это сделал:
Context.parse - очень полезный метод для парсинга строк, содержащих Haxe код, например, из внешнего файла или самостоятельно собранных строк, как на примере выше. Еще есть одна хитрая задачка, которую можно решить только с помощью этого метода, и о ней я как нибудь расскажу. Но, по правде сказать, я недолюбливаю parse и стараюсь использовать его только при крайней необходимости, хотя бы потому, что передаваемые строки могут содержать синтаксические или логические ошибки, да и лишний парсинг отнимает время. Дальше я покажу, как в нашей задаче можно обойтись без parse.
Чтобы “избавиться” от Context.parse, да и для того, чтобы показать силу макросов, напишем все то же, но иначе и сложнее :). Но для начала вернемся к структуре ExprDef, как я говорил, она описывает любое выражение в Haxe, а значит сможет описать и наш Date.fromString(). “Но как это сделать,” - спросите вы, и я вам признаюсь честно, я не знаю. Нет, ну я догадываюсь, некоторые ExpDef я знаю, а остальное можно найти в документации, но я покажу вам максимально простой метод узнать как записать нужное вам выражение. Все очень просто, создадим вот такой метод:
и вызовем, передав ему нужное нам выражение:
Перед вами ни что иное, как запись haxe кода с помощью Expr. Если немного укоротить полученную строку, то выйдет нечто:
ECall(EField(Date, “fromString”), [“2013-01-01”])
Т.е. мы находим поле (метод) fromString у класса Date и вызываем его, передав в качестве параметра массив, содержащий одну строку с текущей датой. Предлагаю сдержать крики “О боже, как все сложно!” и просто взглянуть на метод, который в итоге получился:
Ну, я же говорил ,что сделаем сложнее, вот мы и сделали. Я не стану вас убеждать, что так правильнее или еще что-то, я лишь скажу, что дальше будет еще один вариант, намного проще этого и если вам пока не охота разбираться, что тут и как, сразу переходите дальше, но не забывайте, что с Expr и ExprDef вам все равно придется работать, когда вы дойдете до сложных макросов и обычные parse и makeExpr вам там уже не помогут. Главное, не бояться почаще смотреть, как сам Haxe собирает выражения (метод test выше), и все у вас получится. И не поленитесь внимательно изучить всю строку с ретурном хотя-бы на этом простом примере.
Но и это еще не все! Последний вариант хоть и работает, но заслуженно вызывает испуг у большинства. И хотя Николас (автор языка Haxe) может думать как компилятор и даже вместо него и может заставить разработчиков делать также, однако, быстро стало ясно, что работу с макросами нужно как-то упростить. Результатом данного упрощения стало macro reification, которое упростило синтаксис и работу с макросами.
Вспомните, с чего мы начали: у нас есть строка с датой, и мы хотим передать ее в метод, который распарсит ее и вернет дату. Т.е. в идеале хотелось бы взять и написать return Date.fromString(d.toString()); и мы так и сделаем, или почти так:
По-моему, получилось отлично! Результат getBuildDate4 совпадает с getBuildDate3, а внешне getBuildDate4 явно выглядят лучше. Взгляните на значение переменной е. Это именно то, что возвращал наш первый метод getBuildDate - выражение, содержащее строку с датой. Вся магия в последней строке после оператора return. Сначала мы видим оператор macro, который говорит компилятору, что все, что идет дальше, надо переводить сразу в Expr. Т.е. если написать
Согласитесь, отлично придумано. А чтобы в это выражение встраивать дополнительные значения извне, нужно передавать их с ключом $ вначале, тогда компилятор на это место подставит выражение из переменной, главное, чтобы она (переменная) тоже было типа Expr, в нашем примере это $e, переданная методу fromString. Т.о. весь ужас из нескольких вложеных enum-ов, мы записали все одной строкой: macro Date.fromString($e);, А в Haxe 3 это можно записать еще проще.
У Николаса в блоге есть очень наглядный пример того, как macro reification упростил код макросов, и я не могу его не показать:
До упрощения этот макрос можно было бы записать минимум семью строками кода! Разбор того, что он делает и как работает оставляю на вас, надеюсь, теперь это не составит особого труда. Подсказка и вариант до упрощения тут.
Для первого раза, думаю, достаточно и предлагаю на этом остановиться. Подводя итоги, могу сказать, что теперь вы научились писать простейшие макросы, работать с Expr, узнали несколько полезных методов Context класса, а их на самом деле намного больше, немного познакомились с macro reification и т.д. Что вы не узнали, так это, как создавать свои классы и enum-ы, как редактировать целые классы, дополняя их методами или изменяя существующие и многое другое. Надеюсь, мне хватит сил и я обязательно обо всем этом напишу.
Напоследок, к разбору макроса из блога Николаса, предложу еще попробовать всем написать макрос, который будет сохранять значение файла в строку и работающий как показано ниже:
Отдельная благодарность Александру Кузьменко, Александру Хохлову и SlavaRa за помощью в написании статьи, рецензирование, редактирование и полезную критику.