Макросы Haxe. Вводная статья.
Запись от Dima_DPE размещена 06.05.2013 в 15:18
Цитата:
Все ниже написанное соответствует Haxe 2 и почти верно для Haxe 3. Но работать без изменений будет только во 2-м. Кто адаптирует все примеры на 3-й и поделится с остальными, получит много экспириенса. Так же написанное не претендует на истину в последней инстанции и может содержать ошибки.
Макросы в Haxe используют неоправданно мало программистов, кого-то отпугивает их синтаксис, кто-то просто не знает, что это и зачем. Все возможности макросов отлично описаны в статье метапрограммирование на википедии. В двух словах скажу: макросы позволяют как модифицировать существующий код (самомодификация кода), так и создавать новый (генерация кода). В этой статье мы попробуем генерировать новый код, но в очень ограниченных масштабах. Такие вещи, как генерация классов или enum-ов или редактирование существующего кода, я не затрону, лишь замечу, что все это возможно в пределах Haxe макросов!
Так что же на самом деле макрос в Haxe? Вы не поверите, но макросы это обычные функции. Ну не совсем обычные, но и ничего незнакомого по сравнению с кодом Haxe в них нет. Главные плюсы макросов - это то, что в них, кроме стандартной библиотеки, доступны еще и все классы пакетов neko и haxe.macro и то, что функции-макросы вызываются на этапе компиляции, а не во время выполнения программы и возвращают Haxe код, который и будет выполнен на этапе исполнения программы. Neko дает доступ к файловой системе и вообще к системе в целом, а haxe.macro классы позволяют... Да они все позволяют: создавать новые классы, менять структуру существующих, получать полные данные о всех типах, enum-ах и т.д. в общем полный доступ. Главное знать, что именно вам надо. Все это и делает макросы такими интересными для нас. Но не будем сильно затягивать и посмотрим на пример, который будет запоминать в скомпилированном приложении и выводить дату сборки проекта (без сторонних утилит такую задачу решить довольно сложно и тут на помощь приходит макрос):
import haxe.macro.Context; import haxe.macro.Expr; class Main { #if !macro static function main() { trace(getBuildDate()); // 2013-05-04 14:11:44 } #end @:macro static public function getBuildDate():Expr { var d = Date.now(); return Context.makeExpr(d.toString(), Context.currentPos()); } }
Expr - это просто структура с двумя полями: expr - любое выражение, допустимое в Haxe, но не в привычном виде, а как одно из значений ExprDef enum-а (перечисляемого типа), например EFunction, EVar, ENew, EConst, ECall, EContinue, EReturn и т.д. И pos - указатель на место, где в файле будет расположено выражение. На самом деле pos самому вычислять придется не так часто и для начала запомним, что на его место подставляем Context.currentPos(). Вот как выглядит Position тип:
Теперь, когда Expr перестал быть полной загадкой, вернемся к методу Context.makeExpr. Из названия понятно, что он что-то делает и вот тут лучше, как говорится, один раз увидеть, что-же он делает:
trace(Context.makeExpr(d.toString(), Context.currentPos())); // { expr => EConst(CString(2013-05-04 14:11:44)), pos => #pos(src/Main.hx:8: characters 8-20) }
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 методу. Смотрим, что у меня получилось:
@:macro static public function getBuildDate2():Expr { var d = Date.now(); return Context.parse("Date.fromString('" + d.toString() + "')", Context.currentPos()); }
Context.parse - очень полезный метод для парсинга строк, содержащих Haxe код, например, из внешнего файла или самостоятельно собранных строк, как на примере выше. Еще есть одна хитрая задачка, которую можно решить только с помощью этого метода, и о ней я как нибудь расскажу. Но, по правде сказать, я недолюбливаю parse и стараюсь использовать его только при крайней необходимости, хотя бы потому, что передаваемые строки могут содержать синтаксические или логические ошибки, да и лишний парсинг отнимает время. Дальше я покажу, как в нашей задаче можно обойтись без parse.
Чтобы “избавиться” от Context.parse, да и для того, чтобы показать силу макросов, напишем все то же, но иначе и сложнее . Но для начала вернемся к структуре ExprDef, как я говорил, она описывает любое выражение в Haxe, а значит сможет описать и наш Date.fromString(). “Но как это сделать,” - спросите вы, и я вам признаюсь честно, я не знаю. Нет, ну я догадываюсь, некоторые ExpDef я знаю, а остальное можно найти в документации, но я покажу вам максимально простой метод узнать как записать нужное вам выражение. Все очень просто, создадим вот такой метод:
и вызовем, передав ему нужное нам выражение:
trace(test(Date.fromString("2013-01-01"))); //src/Main.hx:16: { expr => ECall({ expr => EField({ expr => EConst(CIdent(Date)), pos => #pos(src/Main.hx:8: characters 13-17) },fromString), pos => #pos(src/Main.hx:8: characters 13-28) },[{ expr => EConst(CString(2013-01-01)), pos => #pos(src/Main.hx:8: characters 29-41) }]), pos => #pos(src/Main.hx:8: characters 13-42) }
Т.е. мы находим поле (метод) fromString у класса Date и вызываем его, передав в качестве параметра массив, содержащий одну строку с текущей датой. Предлагаю сдержать крики “О боже, как все сложно!” и просто взглянуть на метод, который в итоге получился:
@:macro static public function getBuildDate3():Expr { var d = Date.now(); var p = Context.currentPos(); return { expr:ECall( { expr:EField( { expr:EConst(CIdent("Date")), pos:p }, "fromString"), pos:p }, [ { expr:EConst(CString(d.toString())), pos:p } ]), pos:p }; }
Но и это еще не все! Последний вариант хоть и работает, но заслуженно вызывает испуг у большинства. И хотя Николас (автор языка Haxe) может думать как компилятор и даже вместо него и может заставить разработчиков делать также, однако, быстро стало ясно, что работу с макросами нужно как-то упростить. Результатом данного упрощения стало macro reification, которое упростило синтаксис и работу с макросами.
Вспомните, с чего мы начали: у нас есть строка с датой, и мы хотим передать ее в метод, который распарсит ее и вернет дату. Т.е. в идеале хотелось бы взять и написать return Date.fromString(d.toString()); и мы так и сделаем, или почти так:
@:macro static public function getBuildDate4():Expr { var d = Date.now(); var e = Context.makeExpr(d.toString(), Context.currentPos()); return macro Date.fromString($e); }
то это то же самое, что написать
Согласитесь, отлично придумано. А чтобы в это выражение встраивать дополнительные значения извне, нужно передавать их с ключом $ вначале, тогда компилятор на это место подставит выражение из переменной, главное, чтобы она (переменная) тоже было типа Expr, в нашем примере это $e, переданная методу fromString. Т.о. весь ужас из нескольких вложеных enum-ов, мы записали все одной строкой: macro Date.fromString($e);, А в Haxe 3 это можно записать еще проще.
У Николаса в блоге есть очень наглядный пример того, как macro reification упростил код макросов, и я не могу его не показать:
@:macro static function repeat( e : Expr, eN : Expr ) { return macro for( x in 0...$eN ) $e; }
Для первого раза, думаю, достаточно и предлагаю на этом остановиться. Подводя итоги, могу сказать, что теперь вы научились писать простейшие макросы, работать с Expr, узнали несколько полезных методов Context класса, а их на самом деле намного больше, немного познакомились с macro reification и т.д. Что вы не узнали, так это, как создавать свои классы и enum-ы, как редактировать целые классы, дополняя их методами или изменяя существующие и многое другое. Надеюсь, мне хватит сил и я обязательно обо всем этом напишу.
Напоследок, к разбору макроса из блога Николаса, предложу еще попробовать всем написать макрос, который будет сохранять значение файла в строку и работающий как показано ниже:
Цитата:
Отдельная благодарность Александру Кузьменко, Александру Хохлову и SlavaRa за помощью в написании статьи, рецензирование, редактирование и полезную критику.
Всего комментариев 27
Комментарии
06.05.2013 15:57 | |
Это очень интересно!
|
07.05.2013 02:52 | |
Цитата:
Макросом можно вызвать разрыв мозга
|
07.05.2013 03:11 | |
Котяра, начни со статьи на википедии http://ru.wikipedia.org/wiki/%D0%9C%...BD%D0%B8%D0%B5 . Когда будет понятно зачем метопрограммирование, вопросы зачем нужны макросы отпадут.
|
07.05.2013 09:08 | |
Цитата:
Главные плюсы макросов - это то, что в них, кроме стандартной библиотеки, доступны еще и все классы пакетов neko и haxe.macro и то, что функции-макросы вызываются на этапе компиляции, а не во время выполнения программы и возвращают Haxe код, который и будет выполнен на этапе исполнения программы. Neko дает доступ к файловой системе и вообще к системе в целом, а haxe.macro классы позволяют... Да они все позволяют: создавать новые классы, менять структуру существующих, получать полные данные о всех типах, enum-ах и т.д. в общем полный доступ.
Возможно, макросы в хаксе - это такой бэкдор в "классы пакетов neko и haxe.macro"? Т.е. в рантайме они не доступны? Что же полезного в таком "макросе" и что за "Haxe код" они возвращают? Другим краем сознания я понимаю, что автор всего этого хозяйства, конечно, хотел как лучше и, видимо, он заслуживает респекта. Ну, и конечно, хочется посмотреть на продолжение статьи, возможно, там все прояснится. Цитата:
Макросы в Haxe используют неоправданно мало программистов, кого-то отпугивает их синтаксис, кто-то просто не знает, что это и зачем.
Цитата:
Такие вещи, как генерация классов или enum-ов или редактирование существующего кода, я не затрону, лишь замечу, что все это возможно в пределах Haxe макросов!
|
|
Обновил(-а) alexcon314 07.05.2013 в 09:28
|
07.05.2013 09:30 | |
Ну, не то что не в тему, я как-то ожидал, что немного с другого начнут.
|
07.05.2013 09:32 | |
а все, теперь я, наверное, понял о чем речь
|
07.05.2013 11:29 | |
@СлаваRa
Singleton: можно, и даже более того: https://github.com/imps/singleton Конфиги парсить, "вшивать" можно. Для модели есть даже набор макросов в стандартной библиотеке haxe - SPOD, который по структуре базы данных генерирует классы модели. Для гуя и шаблонизаторов можно использовать макросы, чтобы делать всё type-safe и исключить время, затрачиваемое на парсинг шаблонов/xml во время выполнения программы. Например, вот либа, которая по разметке xml генерит haxe код: https://github.com/RealyUniqueName/StablexUI Макросы позволяют отлавливать ошибки в шаблонах/xml на этапе компиляции и встраивать haxe код прямо в шаблоны, что даёт невероятную гибкость: <!-- компиляция не пройдёт и выдаст ошибку, что alpha должен быть Float, а не String --> <Sprite alpha="wtf_this_should_be_float" /> <!-- можем встраивать произвольный haxe код в атрибуты вместо скучных значений --> <Sprite width="MainClass.SOME_CONST * 2 + Std.random(100)" /> |
|
Обновил(-а) RealyUniqueName 07.05.2013 в 13:55
|
07.05.2013 11:39 | |
@alexcon314
Да, в C макрос - это просто макро команда. В Haxe - это гораздо больше (см мой предыдущий комментарий). Не пугайтесь генерации/модификации классов ) Посмотрите тот же пример с синглтоном: https://github.com/imps/singleton Или генерацию классов модели: http://haxe.org/manual/spod Ещё есть интересный пример: В Haxe нет классических абстрактных классов (есть абстрактные типы данных, но это другое), но с помощью простого макроса в 20 строк кода можно реализовать эту конструкцию языка. |
|
Обновил(-а) RealyUniqueName 07.05.2013 в 15:01
|
07.05.2013 12:38 | |
вот еще бы тему комплишенов для генерации и вшивания кода, развернуть было бы здорово
|
07.05.2013 12:51 | |
ну вот как-то упоминалось о комплишенах для внешних XML\JSON, только ссылку так найти и не получилось
|
07.05.2013 12:58 | |
Должен признать, да, в хаксе это гораздо больше. Пример с синглтоном вполне убедил).
|
07.05.2013 13:14 | |
@Котяра
1. Мне пока не приходилось работать с мак адресами, но вот что с ходу пришло в голову: - с помощью класса sys.io.Process запускаем команду ifconfig (это для линукса. Для винды хз, где смотреть мак) - вытаскиваем из вывода команды мак адрес - если сделаем это в макро-функции, сможем сохранить мак разработчика в коде. 2 и 3. http://haxememes.tumblr.com/post/42315290417 @СлаваRa К сожалению, так и не смог найти этот пример с автодополнением по json/xml, но точно помню, что где-то видел его. Самому мне сейчас в голову не приходит хорошее решение этой задачи. Будет время, подумаю ) |
Последние записи от Dima_DPE