Все нижче написане відповідає Haxe 2 і майже вірно для Haxe 3. Але працювати без змін буде тільки в 2-му. Хто адаптує всі приклади на 3-й і поділиться з іншими, отримає багато експірієнс. Так само написане не претендує на істину в останній інстанції і може містити помилки.
Так що ж насправді макрос в Haxe? Ви не повірите, але макроси це звичайні функції. Ну не зовсім звичайні, але і нічого незнайомого в порівнянні з кодом Haxe в них немає. Головні плюси макросів - це те, що в них, крім стандартної бібліотеки, доступні ще й всі класи пакетів neko і haxe.macro і те, що функції-макроси викликаються на етапі компіляції, а не під час виконання програми і повертають Haxe код, який і буде виконаний на етапі виконання програми. Neko дає доступ до файлової системи і взагалі до системи в цілому, а haxe.macro класи дозволяють. Так вони всі дозволяють: створювати нові класи, змінювати структуру існуючих, отримувати повні дані про всіх типах, enum-ах і т.д. в загальному повний доступ. Головне знати, що саме вам треба. Все це і робить макроси такими цікавими для нас. Але не будемо сильно затягувати і подивимося на приклад, який буде запам'ятовувати в скомпільованому додатку і виводити дату складання проекту (без сторонніх утиліт таке завдання вирішити досить складно і тут на допомогу приходить макрос):
import haxe. macro. Context;
import haxe. macro. Expr;
@: Macro static public function getBuildDate # 40; # 41; : Expr # 123;
var d = Date. now # 40; # 41; ;
return Context. makeExpr # 40; d. toString # 40; # 41 ;. Context. currentPos # 40; # 41; # 41; ;
# 125;
# 125;
Функція getBuildDate і є наш макрос. Відразу звертаємо увагу на мета тег @: macro на початку функції - це відмітний знак, що говорить компілятору, що виконувати цю функцію треба під час компіляції! Наша функція повертає тип Expr, про нього трохи пізніше. Далі в методі getBuildDate створюється звичайний Date з поточною датою, який переводиться в рядок і передається в "магічний" Context.makeExpr і ще менш зрозумілий метод Context.currentPos. І тут варто повернутися до типу Expr, його визначення легко знайти в коді модуля haxe.macro.Expr
typedef Expr = # 123;
var expr. ExprDef;
var pos. Position;
# 125;
Expr - це просто структура з двома полями: expr - будь-який вираз, допустиме в Haxe, але не в звичному вигляді, а як одне зі значень ExprDef enum-а (перераховується типу), наприклад EFunction, EVar, ENew, EConst, ECall, EContinue , EReturn і т.д. І pos - покажчик на місце, де у файлі буде розташовано вираз. Насправді pos самому вираховувати доведеться не так часто і для початку запам'ятаємо, що на його місце підставляємо Context.currentPos (). Ось як виглядає Position тип:
typedef Position = # 123;
var file. String;
var min. Int;
var max. Int;
# 125;
Тепер, коли Expr перестав бути повною загадкою, повернемося до методу Context.makeExpr. З назви зрозуміло, що він щось робить і ось тут краще, як то кажуть, один раз побачити, що-ж він робить:
Поверніться до опису структури Expr і переконайтеся, що перед нами дійсно Expr, що містить одну строкову константу Econst (CString ()) з датою складання проекту і розташовану в Main.hx на 8 рядку, і займає 8-20 символи, і це дуже важливо! Погляньте на 8-й рядок першого прикладу з макросом, там написано:
trace # 40; getBuildDate # 40; # 41; # 41; ;
Ось і сталося: ви розібрали і, сподіваюся, зрозуміли свій, можливо, перший, але далеко не останній макрос. Хоча впевнений, що залишилося питання: "Що за магічні #if! Macro перед main функцією?". Вся справа в тому, що macro функції сильно впливають на поведінку класу, в якому вони визначені, та й обсяги імпорту, доступні макро функцій, часто недозволені решті коду, ось і довелося так звиватися, щоб вмістити все в один модуль. Але моя вам порада (та й взагалі так правильніше): пишіть макроси в окремо відведених для цього класах, не змішуючи їх з іншим кодом. Далі я буду робити тільки так, але "змішаний" варіант я не міг не показати.
Зізнаюся, в цій статті я хотів написати ще купу всяких макросів, щоб заманити якомога більше людей, але зрозумів, що ваші знання дуже малі, і робити щось круте занадто рано. А ось ускладнити наш перший макрос - саме час!
Завдання ми собі поставили не найбільшу просту, а навіть трохи складну. makeExpr нам уже не допоможе, він вміє працювати тільки з базовими і переліком типами (Int, Float, String, Bool, Array і анонімні об'єкти, складені з цих типів), саме час звернутися до Context.parse методу. Дивимося, що у мене вийшло:
@: Macro static public function getBuildDate2 # 40; # 41; : Expr # 123;
var d = Date. now # 40; # 41; ;
return Context. parse # 40; "Date.fromString ( '" + d. ToString # 40; # 41; + " ')". Context. currentPos # 40; # 41; # 41; ;
# 125;
Видно, що Context.parse першим параметром прийняв рядок, що містить Haxe код, а ось на її результат раджу поглянути самим, лише підкажу, як я це зробив:
var date = getBuildDate2 # 40; # 41; ;
trace # 40; date # 41; ;
trace # 40; Type. typeof # 40; date # 41; # 41; ;
Context.parse - дуже корисний метод для парсинга рядків, що містять Haxe код, наприклад, із зовнішнього файлу або самостійно зібраних рядків, як на прикладі вище. Ще є одна хитра задачка, яку можна вирішити тільки за допомогою цього методу, і про неї я як небудь розповім. Але, по правді сказати, я недолюблюю parse і намагаюся використовувати його лише за крайньої необхідності, хоча б тому, що передаються рядки можуть містити синтаксичні або логічні помилки, та й зайвий парсинг забирає час. Далі я покажу, як в нашій задачі можна обійтися без parse.
Щоб "позбутися" від Context.parse, та й для того, щоб показати силу макросів, напишемо все той же, але інакше і складніше :). Але для початку повернемося до структури ExprDef, як я говорив, вона описує будь-який вираз в Haxe, а значить зможе описати і наш Date.fromString (). "Але як це зробити," - запитаєте ви, і я вам зізнаюся чесно, я не знаю. Ні, ну я здогадуюся, деякі ExpDef я знаю, а решта можна знайти в документації, але я покажу вам максимально простий метод дізнатися як записати потрібну вам вираз. Все дуже просто, створимо ось такий метод:
@: Macro static function test # 40; e: Expr # 41; # 123;
trace # 40; e # 41; ;
return e;
# 125;
і викличемо, передавши йому потрібне нам вираз:
@: Macro static public function getBuildDate3 # 40; # 41; : Expr # 123;
var d = Date. now # 40; # 41; ;
var p = Context. currentPos # 40; # 41; ;
return # 123; expr: ECall # 40;
# 123; expr: EField # 40;
# 123; expr: EConst # 40; CIdent # 40; "Date" # 41; # 41 ;. pos: p # 125; ,
"FromString" # 41; ,
pos: p # 125; ,
# 91; # 123; expr: EConst # 40; CString # 40; d. toString # 40; # 41; # 41; # 41 ;. pos: p # 125; # 93; # 41; ,
pos: p # 125; ;
# 125;
Ну, я ж казав, що зробимо складніше, ось ми і зробили. Я не стану вас переконувати, що так правильніше або ще щось, я лише скажу, що далі буде ще один варіант, набагато простіше цього і якщо вам поки не охота розбиратися, що тут і як, відразу переходите далі, але не забувайте, що з Expr і ExprDef вам все одно доведеться працювати, коли ви дійдете до складних макросів і звичайні parse і makeExpr вам там вже не допоможуть. Головне, не боятися частіше дивитися, як сам Haxe збирає вираження (метод test вище), і все у вас вийде. І не полінуйтеся уважно вивчити всю рядок з ретурном хоча-б на цьому простому прикладі.
Згадайте, з чого ми почали: у нас є рядок з датою, і ми хочемо передати її в метод, який розпарсити її і поверне дату. Тобто в ідеалі хотілося б взяти і написати return Date.fromString (d.toString ()); і ми так і зробимо, або майже так:
@: Macro static public function getBuildDate4 # 40; # 41; : Expr # 123;
var d = Date. now # 40; # 41; ;
var e = Context. makeExpr # 40; d. toString # 40; # 41 ;. Context. currentPos # 40; # 41; # 41; ;
return macro Date. fromString # 40; $ e # 41; ;
# 125;
По-моєму, вийшло відмінно! Результат getBuildDate4 збігається з getBuildDate3, а зовні getBuildDate4 явно виглядають краще. Погляньте на значення змінної е. Це саме те, що повертав наш перший метод getBuildDate - вираз, що містить рядок з датою. Вся магія в останньому рядку після оператора return. Спочатку ми бачимо оператор macro, який говорить компілятору, що всі, що йде далі, треба переводити відразу в Expr. Тобто якщо написати
то це те ж саме, що написати
# 123; expr: EConst # 40; CString # 40; "Foo" # 41; # 41 ;. pos: Context. currentPos # 40; # 41; # 125;
Погодьтеся, відмінно придумано. А щоб в цей вислів вбудовувати додаткові значення ззовні, потрібно передавати їх з ключем $ спочатку, тоді компілятор на це місце підставить вираз із змінної, головне, щоб вона (змінна) теж було типу Expr, в нашому прикладі це $ e, передана методу fromString . Т.ч. весь жах з декількох вкладених enum-ів, ми записали всі одним рядком: macro Date.fromString ($ e) ;, А в Haxe 3 це можна записати ще простіше.
У Ніколаса в блозі є дуже наочний приклад того, як macro reification спростив код макросів, і я не можу його не показувати:
@: Macro static function repeat # 40; e. Expr, eN. Expr # 41; # 123;
return macro for # 40; x in 0. $ eN # 41; $ E;
# 125;
До спрощення цей макрос можна було б записати мінімум сім'ю рядками коду! Розбір того, що він робить і як працює залишаю на вас, сподіваюся, тепер це не складе особливих труднощів. Підказка і варіант до спрощення тут.
Для першого разу, думаю, досить і пропоную на цьому зупинитися. Підводячи підсумки, можу сказати, що тепер ви навчилися писати найпростіші макроси, працювати з Expr, дізналися кілька корисних методів Context класу, а їх насправді набагато більше, трохи познайомилися з macro reification і т.д. Що ви не дізналися, так це, як створювати свої класи і enum-и, як редагувати цілі класи, доповнюючи їх методами або змінюючи існуючі та багато іншого. Сподіваюся, мені вистачить сил і я обов'язково про все це напишу.
Наостанок, до розбору макросу з блогу Ніколаса, запропоную ще спробувати всім написати макрос, який буде зберігати значення файлу в рядок і працює як показано нижче:
var str: String = MyMacros. getFileContent # 40; "Readme.txt" # 41; ;
Окрема подяка Олександру Кузьменко. Олександру Хохлова і SlavaRa за допомогою в написанні статті, рецензування, редагування та корисну критику.