Ми вже вміємо будувати цитату з окремих частин і запускати її. Цитата - це просто послідовність коду, тож знайомі нам слова для роботи з послідовностями працюють і з нею:
USING: kernel math prettyprint quotations sequences ;
{ [ 1 ] [ 2 + ] [ 3 * ] } concat >quotation call . ! => 9
[ 1 + ] dup append . ! => [ 1 + 1 + ]
append поєднує дві цитати, concat розгортає масив із них, map перетворює дані на цитати, а >quotation робить результат таким, що його можна викликати. MACRO: бере саме цю навичку й переносить її на час компіляції.
Визначення MACRO: має вигляд звичайного слова, але його тіло виконується тоді, коли наша програма компілюється, а не тоді, коли вона працює. Тіло має залишити на стеку одну цитату. Далі компілятор вставляє цю цитату в місце виклику: він замінює виклик макроса кодом, який створило тіло. Під час виконання макроса вже немає, є лише згенерований код.
USING: kernel macros sequences ;
MACRO: twice ( quot -- spliced )
dup append ;
Де б ми не написали [ 1 + ] twice, компілятор:
[ 1 + ];dup append будує цитату [ 1 + 1 + ];1 + 1 +, на це місце.Тож слово, яке викликає twice, компілюється так, ніби ми написали 1 + 1 + власноруч:
: inc-twice ( x -- y ) [ 1 + ] twice ;
5 inc-twice . ! => 7
Ключова відмінність: звичайне слово, яке приймає цитату, викликає її (call) під час виконання. Макрос не викликає нічого: він віддає компілятору новий код, щоб той скомпілював його замість виклику.
Оскільки тіло виконується під час компіляції, вхідні дані макроса мають бути відомі саме тоді. У місці виклику вони мають бути літералами: цитата [ … ], число, масив, рядок тексту (англ. string). Значення, яке існує лише під час виконання, макросу не передати.
! DOCTEST: SKIP — illustrative call sites, not standalone code
[ 1 + ] twice ! ok: the quotation is a literal
[ 1 + ] 3 repeat-quot ! ok: the quotation and the number are literals
Тіло макроса - звичайний код, який повертає цитату. Більшість роботи роблять слова, які ми вже зустрічали:
| щоб побудувати… | використовуємо |
|---|---|
[ A B ] з [ A ] і [ B ]
|
append |
| одну цитату з масиву цитат | concat >quotation |
| цитату для кожного елемента послідовності |
map, потім concat >quotation
|
Ще два слова, з sequences і kernel, доповнюють цей набір.
<repetition> (з sequences) створює послідовність із n копій одного елемента. Її стековий ефект - ( length elt -- repetition ): кількість стоїть першою. Результат - віртуальна послідовність, тож розгортаємо її звичним способом:
3 [ 1 + ] <repetition> concat >quotation . ! => [ 1 + 1 + 1 + ]
curry (з kernel) додає значення на початок цитати й створює нову цитату, яка кладе це значення на стек першим:
3 [ + ] curry . ! => [ 3 + ]
{ 1 2 3 } [ . ] curry . ! => [ { 1 2 3 } . ]
curry дає змогу вбудувати в цитату один конкретний елемент: саме він перетворює { 10 20 30 } і [ + ] на [ 10 + ] [ 20 + ] [ 30 + ].
У деяких мовах макросам потрібен спеціальний синтаксис quote/unquote та додаткові механізми, щоб уникати конфліктів імен. У Factor нічого цього не потрібно: цитата - це вже послідовність розпізнаних слів, адже парсер знайшов кожне dup, + й append, коли прочитав [ … ]. Тож тіло макроса - це просто операції з послідовностями, які ми вже виконуємо, і які до того ж повертають цитату.
$[ … ]
Макрос повертає цитату, щоб її вставили у виклик. Коли ж нам потрібне звичайне значення, обчислене під час парсингу, $[ code ] виконує code, поки читається файл, і вкладає результат у код як літерал:
! DOCTEST: SKIP — compile-time value, shown for contrast
USING: calendar literals ;
CONSTANT: build-time $[ now ]
$[ … ] походить із модуля literals, який збирає слова для підстановки результатів, обчислених під час парсингу, у літерали. Згадуємо його для повноти. Усі чотири трафарети, які ми напишемо (twice, repeat-quot, compose-many і each-literal), - це оголошення MACRO:.
Ми підтримуємо набір інструментів для генерації коду на сигнальній станції. Кожен макрос - це трафарет, невеликий шаблон, який компілятор використовує, щоб відтискати спеціалізований код у кожному місці виклику.
Ми пишемо лише чотири макроси. Файл тестів визначає невеликі слова-обгортки, які викликають наші макроси (макрос можна спостерігати лише через код, який він генерує), а потім перевіряє результати виконання цих обгорток. Обгортки залишаємо без змін.
Для кожного завдання тіло отримує літеральні вхідні дані місця виклику на стеку й повинно залишити один quotation. Рядки «compiles to» показують лінійний код, який має згенерувати наша quotation.
twice приймає літеральну quotation і повертає quotation, що запускає оригінал
двічі підряд.
! : inc-twice ( x -- y ) [ 1 + ] twice ;
! body receives [ 1 + ] -> must build [ 1 + 1 + ]
5 inc-twice . ! => 7
repeat-quot приймає літеральну quotation і літеральну кількість n
та повертає quotation, що запускає оригінал n разів підряд.
n = 0 повертає порожню quotation [ ].
! : inc-thrice ( x -- y ) [ 1 + ] 3 repeat-quot ;
! body receives [ 1 + ] 3 -> must build [ 1 + 1 + 1 + ]
5 inc-thrice . ! => 8
compose-many приймає літеральний масив quotation і повертає
одну quotation, що запускає кожну з них по черзі. Порожній масив
повертає порожню quotation.
! : pipeline ( x -- y ) { [ 1 + ] [ 2 * ] [ 1 - ] } compose-many ;
! body receives { [ 1 + ] [ 2 * ] [ 1 - ] } -> must build [ 1 + 2 * 1 - ]
5 pipeline . ! => 11
each-literal приймає літеральну послідовність і quotation. Він
повертає quotation, що для кожного елемента послідовності кладе
цей елемент на стек, а потім запускає quotation. Ітерація
відбувається під час компіляції, тож згенерований код буде лінійним.
! : sum-three ( -- n ) 0 { 10 20 30 } [ + ] each-literal ;
! body receives { 10 20 30 } [ + ] -> must build [ 10 + 20 + 30 + ]
sum-three . ! => 60
Це найструктурніше із чотирьох завдань: і довжина послідовності, і значення її елементів визначають згенерований код. У підказках ми покроково розберемо, як його побудувати.
Зареєструйтеся на Exercism, щоб вивчати й опановувати Factor, а також 47 концепцій163 вправи та справжнє наставництво від людей, і все це безкоштовно.