Мистецтво метапрограмування, Частина 2: метапрограмування з використанням Scheme, Linux, Операційні системи, статті

Зміст



У статті “Мистецтво метапрограмування, частина 1: Введення в метапрограмування“:



У цій статті наводяться подробиці програмування макросів Scheme і пояснюється, як вони можуть значно полегшити масштабні завдання програмування.


Написання макросів syntax-case в Scheme


Хоча макроси syntax-case не є стандартною частиною Scheme, вони є найбільш широко використовуваним типом макросів, що дозволяє гігієнічні і не гігієнічні форми, і є дуже близькими до стандартних макросам syntax-rules.


Макроси syntax-case мають наступний формат (лістинг 1):


Лістинг 1. Загальний формат макросів syntax-case




(define-syntax macro-name
(lambda (x) (Syntax-case x (інші ключові слова, якщо є)
( ;; Перший шаблон
(macro-name macro-arg1 macro-arg2) ;; Розширення макросу (одна або декілька форм) ;; (Syntax – це зарезервоване слово) (Syntax (розширення макросу знаходиться тут))
)
( ;; Другий шаблон – версія з одним аргументом
(macro-name macro-arg1) ;; Розширення макросу (Syntax (розширення макросу знаходиться тут))
)
)))

Цей формат визначає macro-name як ключове слово, яке використовується для перетворення. lambda – Це функція, яка використовується для перетворення виразу x в його розширення.


syntax-case приймає вираз x в якості свого першого аргументу. Другий аргумент – це список ключових слів, які вставляються дослівно в синтаксичні шаблони. Інші ідентифікатори, що використовуються в шаблонах, будуть працювати як змінні шаблону. Потім в syntax-case вказується послідовність комбінацій шаблон / перетворювач. syntax-case обробляє кожну з них, намагаючись зіставити вхідну форму шаблону і, при збігу, виконує відповідне розширення.


Розглянемо простий приклад. Припустимо, що ми хочемо написати більш детальну версію оператора if, Ніж пропоновану в Scheme. І припустимо, що ми хочемо знайти і повернути більше з двох чисел. Код буде виглядати приблизно так:


(if (> a b) a b)


Для програмістів, не працювали з Scheme, буде дивним не бачити текстових вказівок гілки “then” і гілки “else”. Щоб вирішити це питання, ми можемо створити нашу власну версію оператора if, В якої додаються ключові слова “then” і “else”. Код буде виглядати так:


(my-if (> a b) then a else b)


У лістингу 2 приведений макрос для виконання цієї операції:


Лістинг 2. Макрос для визначення розширеної версії оператора if




;; Визначити my-if як макрос
(define-syntax my-if
(lambda (x) ;; Встановити, що “then” і “else” – це ключові слова
(syntax-case x (then else)
( ;; Шаблон для відповідності
(my-if condition then yes-result else no-result) ;; Перетворювач
(syntax (if condition yes-result no-result))
)
)))

При виконанні цього макрос буде зіставляти вираз my-if з шаблоном наступним чином (іншими словами, відповідність виклику макросу шаблону визначення макросу):






(my-if  (> a b)  then     a      else    b)
/ / / / / /
/ / / / / /
v v v v v v
(my-if condition then yes-result else no-result)

В перетворюючої вираженні скрізь, де зустрінеться слово condition, Воно буде замінено на (> a b). Не має значення, що (> a b) – Це список. Це простий елемент, поміщений в список, і розглядається він як окрема сутність у шаблоні. Результуюче синтаксичне вираження просто переставляє кожну з цих частин в новому вираженні.


Це перетворення відбувається перед виконанням під час так званого макророзширення. У багатьох заснованих на компіляторах реалізаціях Scheme макророзширення відбувається під час компіляції. Це означає, що макроси виконуються тільки один раз, на початку програми або під час компіляції, і ніколи не обчислюються повторно. Отже, наш оператор my-if не вносить жодних накладних витрат – він перетвориться в простій if під час виконання.


У наступному прикладі ми виконаємо відомий макрос swap!. Це буде простою макрос, призначений для перестановки значень двох ідентифікаторів. У лістингу 3 наведено приклад використання макросу.


Лістинг 3. Використання макросу swap! для перестановки значень ідентифікаторів




(define a 1)
(define b 2)
(swap! a b)
(display “a is now “)(display a)(newline)
(display “b is now “)(display b)(newline)

Наступний простий макрос (лістинг 4) реалізує перестановку шляхом визначення нової тимчасової змінної:


Лістинг 4. Визначення вашого власного макросу swap!




;; Визначити новий макрос
(define-syntax swap!
(lambda (x) ;; Тут ми не використовуємо ключових слів
(syntax-case x ()
(
(swap! a b)
(syntax
(let ((c a))
(set! a b)
(set! b c)))
)
)))

Тут визначається нова змінна з ім’ям з. Але що станеться, якщо один з переставляються аргументів буде називатися з?


syntax-case вирішує цю проблему, замінюючи змінну з унікальним, невживаних ім’ям змінної при розширенні макросу. Отже, синтаксичний перетворювач сам про все потурбується.


Зверніть увагу на те, що syntax-case не замінює let, Тому що let – Це певний глобально ідентифікатор.


Техніка заміни імен змінних не конфліктуючими іменами називається гігієною; використовує цю техніку макрос називається гігієнічним макросом. Гігієнічні макроси можуть безпечно використовуватися скрізь без побоювань конфлікту з існуючими іменами змінних. Для широкого кола завдань метапрограмування ця можливість робить макроси більш передбачуваними і зручними в роботі.


Знайомство з ідентифікаторами


Хоча гігієнічні макроси роблять подаються в макросі імена змінних безпечними, існують ситуації, коли ви хочете, щоб макрос був не гігієнічним. Наприклад, припустимо, що ви хочете створити макрос, що визначає змінну в області видимості, яка використовується викликає макрос програмою. Такий макрос повинен бути не гігієнічним, оскільки він втручається в простір імен користувача коду. Однак у багатьох випадках ця здатність є корисною.


В якості простого прикладу, припустимо, що ви хочете написати макрос, який визначає кілька математичних констант для використання всередині макросу (так, це можна було б зробити з використанням інших коштів, але я роблю саме так для прикладу). Припустимо, ми хочемо визначити числа pi і e, Використовуючи наступний виклик макросу (лістинг 5):


Лістинг 5. Виклик макроса визначення математичних констант




(with-math-defines
(* pi e))

Якщо б ми спробували записати його як попередні макроси, він би не працював:


Лістинг 6. Не працюючий макрос визначення математичних констант




(define-syntax with-math-defines
(lambda (x)
(syntax-rules x ()
(
(with-math-defines expression)
(syntax
(let ( (pi 3.14) (e 2.71828) )
expression))
)
)))

Цей фрагмент не працює. Причина цього, як згадувалося раніше, полягає в тому, що Scheme буде перейменовувати pi і e, Для того щоб вони не конфліктували з іншими іменами в оточуючих або вкладених областях видимості. Отже, вони отримають нову назву та код (* pi e) буде посилатися на невизначені змінні. Нам необхідний спосіб записувати літеральние символи, які можуть використовуватися розробником, що викликає макрос.


Для запису в макросі коду, який би не модифікувався автоматично (гігієна) системою Scheme, код повинен бути перетворений зі списку символів у синтаксичний об’єкт, який можна було б потім присвоїти змінної шаблону і вставити в перетворене вираз. Для цього ми будемо використовувати with-syntax, Який по суті є оператором “let” для макросів. Він має аналогічний основний формат, але використовується для присвоєння синтаксичних об’єктів змінним шаблону.


Для створення нової змінної шаблону ви повинні бути здатні транслювати символи і вирази в обох напрямках між поданням до списку (спосіб запису синтаксису) і більш абстрактним поданням синтаксичного об’єкта. Ці перетворення виконують такі функції:



У даному прикладі для отримання літерального значення в змінній шаблону ви повинні використовувати комбінацію синтаксису і syntax-object->datum. Потім ви могли б попрацювати з виразом і використовувати datum->syntax-object для отримання його назад у вигляді синтаксичного об’єкта, який можна присвоїти змінній шаблону в with-syntax. Потім в кінцевому вираженні перетворення нова змінна шаблона може бути використана як будь-яка інша змінна.


По суті, ви перетворюєте синтаксис Scheme в список, яким можна керувати, працюєте з цим списком і перетворюєте його назад в синтаксис вираження Scheme для виводу.


У лістингу 7 наведено визначення макросу, який використовує ці функції для визначення математичних символів:


Лістинг 7. Працюючий макрос визначення математичних констант




(define-syntax with-math-defines
(lambda (x)
(syntax-case x ()
( ;; Шаблон
(with-math-defines expression)
;; With-syntax визначає нові змінні шаблону
(with-syntax
( (Expr;; нова змінна шаблона ;; Перетворити вираз в синтаксичний об’єкт
(datum->syntax-object ;; Syntax – місцева магія
(syntax k) ;; Вираз для перетворення
`(let ( (pi 3.14) (e 2.72)) ;; Вставити код для змінної шаблону “expression” ;; Сюди.
,(syntax-object->datum (syntax expression)))))) ;; Використовувати нову створену змінну шаблону “expr” ;; Як кінцеве вираження
(syntax expr))
)
)))

Якщо ви не знайомі з Scheme, зворотній лапки, звана квазікавичкой, схожа на оператор “лапки” за винятком того, що дозволяє включати дані, що не мають лапок, якщо вони починаються з комою (Яка називається оператором закриття лапок). Це дозволяє нам поєднати вираз з нашим фрагментом шаблонного коду, потім все це справа перетворити назад в синтаксичний об’єкт як кінцеве перетворення.


Оскільки ми явно з’єднали нові змінні з існуючим синтаксичним об’єктом, вони не можуть бути перейменовані. Також зверніть увагу на те, що вираз (syntax k) в datum->syntax-object необхідно, але, по суті, безглуздо. Воно використовується для активізації маленької “магії” в синтаксичному процесорі, для того щоб функція datum->syntax-object знала, який контекст висловлювання має бути в ній оброблений. Завжди записується як (syntax k).


Проблема негігіеніческіх макросів полягає в тому, що визначаються змінні можуть перезаписати інші змінні і бути перезаписані іншими змінними в коді. Це робить змішування негігіеніческіх макросів особливо небезпечним, оскільки макроси не будуть знати, які змінні використовують інші макроси, і можуть поміняти значення змінних один одного. Отже, негігіеніческіе макроси повинні використовуватися тільки тоді, коли немає іншого способу вирішити задачу з використанням звичайних функцій або гігієнічних макросів, прчем в таких ситуаціях символічні визначення макросів повинні бути ретельно задокументовані.


Створення стереотипних макросів


Значний обсяг у великих програмах займає стереотипний код, написання якого є трудомістким процесом. Згодом, якщо в ньому виявиться помилка, дуже і дуже важко буде знайти кожен примірник такого коду і виправити його. Це означає, що стереотипний код – це одне з небагатьох місць, де негігіеніческіе макроси корисні.


Значна частина стереотипного коду просто встановлює змінні, які потім будуть використовуватися у вашій функції. Отже, стереотипні макроси повинні визначати великий набір звичайних присвоєнь, а також, можливо, й інші допоміжні завдання.


Припустимо, що ми створюємо CGI-додаток, що складається з декількох незалежних CGI-сценаріїв. У більшості CGI-додатків велика частина змінних стану зберігається в базі даних, але тільки ID сесії передається в сценарій через куки.


Однак, практично в кожній сторінці ми повинні знати іншу стандартну інформацію (наприклад, ім’я користувача, номер групи, поточну виконувану користувачем задачу і будь-яку іншу стосується справи інформацію). Крім того, ми повинні перенаправляти користувачів, якщо вони не мають відповідного куки. У лістингу 8 наведено код, який міг би бути стереотипним (функції гіпотетичного Web-сервера починаються з webserver:):


Лістинг 8. Стереотипний код для Web-додатки




(define (handle-cgi-request req)
(let (
(session-id (webserver:cookie req “sessionid”)))
(if (not (webserver:valid-session-id session-id))
(webserver:redirect-to-login-page)
(let (
(username (webserver:username-for-session session-id))
(group (webserver:group-for-user username))
(current-job (webserver:current-job-for-user username))) ;; Оброблювальний код поміщається сюди
))))

Хоча дещо з цього може виконуватися в процедурі, присвоєння виразно немає. Однак ми можемо записати більшість з них в макросі. Макрос може бути реалізований так:


Лістинг 9. Макрос зі стереотипним кодом




(define-syntax cgi-boilerplate
(lambda (x)
(syntax-case x ()
(
(cgi-boilerplate expr)
(datum->syntax-object
(syntax k)
`(let (
(session-id (webserver:cookie req “sessionid”)))
(if (not (webserver:valid-session-id session-id))
(webserver:redirect-to-login-page)
(let (
(username (webserver:username-for-session session-id))
(group (webserver:group-for-user username))
(current-job (webserver:current-job-for-user username)))
,(syntax-object->datum (syntax expr))))))
)
)))

Тепер ми можемо створити нові форми, засновані на нашому стереотипному коді, наступним чином:






(define (handle-cgi-request req)
(cgi-boilerplate
(begin ;; Виконати будь-які бажані дії
)))

Крім того, оскільки ми не визначаємо наші змінні явно, додавання нових визначень змінних в наш стереотипний код не буде впливати на угоди на його виклик, тому нові можливості можуть бути додані без необхідності створювати нову функцію.


У будь-якому великому проекті неминуче існують шаблони, які не можна виділити в функції, головним чином через створюваних присвоєнь. Використання стереотипних макросів може значно полегшити обслуговування такого стереотипного коду.


Також можуть бути створені й інші стандартні макроси, що використовують змінні, визначені в стереотипному коді. Використання подібних макросів значно зменшує роботу по введенню коду, оскільки вам не потрібно постійно записувати й переписувати присвоєння змінних, розгалуження і передані параметри. Також в такому коді зменшується ймовірність помилок.


Але знайте, що стереотипні макроси не є панацеєю. Може виникнути багато проблем, включаючи такі:



Цих проблем в основному можна уникнути, виконавши кілька дій, що відносяться до ваших стереотипним макросам:



Використання макросів для предметно-орієнтованих мов


У програмуванні дійсно часто необхідний маленький предметно-орієнтована мова. Існує багато прикладів таких використовуваних в даний час мовами:



Ці мови не обов’язково повинні бути повними по Тюрінгії (тобто, мають обчислювальну потужність еквівалентну універсальної машині Тюрінга – іншими словами, система і універсальна машина Тюрінга можуть емулювати один одного). Їх загальною властивістю є те, що всі вони мають багато неявних припущень і неявних станів, які виражалися б явно в мовах програмування загального призначення. Scheme дозволяє використовувати найкращі якості обох підходів, будучи здатним визначати макроси, що працюють як спеціалізовані предметно-орієнтовані мови.


В якості першого прикладу розглянемо файл конфігурації безпеки, що описує різні зони безпеки, визначені в конфігураційному файлі. Ми визначимо декілька різних зон безпеки, кожна з яких буде мати різні права доступу і обмеження.


Багато системи вже мають декларативну безпеку. Зокрема, J2EE має деякі можливості декларативною безпеки, які ми збираємося розглянути, наприклад:


Лістинг 10. Можливості декларативною безпеки в J2EE




<![CDATA[
<security-constraint>
<web-resource-collection>
<web-resource-name>Test Resource</web-resource-name>
<description>This is an example Resource</description>
<url-pattern>/Test</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>USERS</role-name>
</auth-constraint>
</security-constraint>
]]>

У цьому коді ми обмежуємо доступ до певного URL на основі заданої ролі користувача і вказуємо, який механізм аутентифікації використовувати для незареєстрованого в системі користувача. Це можна зробити подібним способом в макросі Scheme. Ми могли б визначити макрос, який дозволив би нам зробити щось подібне наступного (макрос декларативною безпеки):






(resource “Test Resource” “This is an example resource” “/Test”
(auth-constraints (role “USERS”)))

У лістингу 11 наведено приклад визначення макросу для попереднього виклику (всі функції, що починаються з префікса webserver:, Є гіпотетичними функціями, наданими Web-сервером):


Лістинг 11. Макрос декларативною безпеки




;; Цей макрос створює висловлювання, що перевіряють коректність ;; Повноважень аутентифікації у змінній “credentials”, ;; Видають звіт і перенаправляють користувачів при неавторизованому доступі.
(define-syntax auth-constraints
(lambda (x)
(syntax-case x (auth-constraints time role)
( ;; Обробка обмежень по одному ;; В операторі (begin).
(auth-constraints constraint1 constraint2 …)
(syntax
(begin
(auth-constraints constraint1)
(auth-constraints constraint2 …)))
)
( ;; Розширення для механізму перевірки ролі ;; (“Credentials” визначені в макросі “resource” нижче)
(auth-constraints (role rolename …))
(syntax
(if
(not
(webserver:is-in-role-list credentials (list rolename …)))
(webserver:report-unauthorized)
#f))
)
( ;; Дозволяє перевірку на основі часу
(auth-constraints (time beginning ending))
(syntax
(let (
(now (webserver:getunixtime)))
(if
(or (< now beginning) (> now ending))
(webserver:report-unauthorized) #f)))
)
( ;; Невідомий випадок – припустимо, що він закодований або перетворений ;; Іншим макросом
(auth-constraints unknown)
(syntax unknown)
)
)))
;; Кожне визначення ресурсу розширює функцію для перевірки ;; Повноважень. Воно розташовується у вигляді ярусів над певними вище макросами, ;; Які складають тіло функції перевірки повноважень. ;; Настроюється параметр “credentials”, який використовується в ;; Зазначених вище виразах
(define-syntax resource
(lambda (x)
(syntax-case x ()
(
(resource name description url security-features)
(with-syntax
( ;; Побудова функції для перевірки інформації безпеки
(security-function
(datum->syntax-object
(syntax k)
`(lambda (credentials)
,@(syntax-object->daturm (syntax security-features))))
(syntax
(webserver:add-security-function
name description url security-function)))))))

Ці макроси вимагають невеликого роз’яснення. Перш за все, присутня нова конструкція – . Це позначення по суті означає “повторювати попереднє”. Воно може бути використане і в шаблоні макросу і в вираженні.


Макрос resource в основному створює функцію для обробки повноважень безпеки і потім передає їх в якості аргументу в webserver:add-security-function. Це функція з одним аргументом, credentials, Який буде використовуватися макросом auth-constraints.


Макрос auth-constraints трохи складніший. Він може мати один з двох форматів – приймати або одне обмеження для обробки, або список обмежень. Перший розділ макросу розбиває список обмежень на кілька одиночних обмежень. Позначення використовується для вказівки можливого продовження аналогічних форматів. Ми користуємося перевагою, що полягає в тому, що після розширення макросу отриманий макрос розширюється знову до тих пір, поки розширення не закінчаться. Якщо ви простежите за ітераційними розширеннями auth-constraints, То побачите, що він і справді розширюється до списку індивідуальних макросів auth-constraints, Які потім обробляються окремо, використовуючи залишилися формати макросу.


auth-constraints має дві додаткові функції, які не використовуються в прикладі. Перша – це заснований на часі механізм авторизації, а друга – здатність до подальшого розширення іншими макросами і кодом. Заснований на часі механізм авторизації – це просто приклад того, як кілька типів обмежень можуть бути додані до цього механізму; функція розширення буде використана в наступному прикладі.


Ці макроси будуть розширювати наші оголошення параметрів безпеки в наступне (лістинг 12):


Лістинг 12. Розширення декларативною безпеки в Scheme




(webserver:add-security-function
“Test Resource”
“This is an example resource”
“/Test”
(lambda (credentials)
(begin
(if (not (webserver:is-in-role-list credentials (list “USERS”)))
(webserver:report-unauthorized)
#f))))

Виникає два очевидних питання:



Є дві причини, які роблять використання макросу кращим, ніж така мова даних як XML, для файлу декларативною безпеки:



Якщо перша функція просто корисна, то друга дійсно варто того, щоб нею користуватися. Оскільки макрос розширюється в звичайний код в будь-якому випадку, ви завжди можете перейти назад до імперативного програмування, якщо декларативний мова не підходить під ваші вимоги. Фактично, якщо перетворення добре документовано, ви навіть можете змішувати декларативні і імперативні оператори у вашій конфігурації.


Припустимо, наприклад, що ви хочете перевірити наявність домену, з якого прийшов користувач, в списку заборонених IP-адрес. Ось як ми можемо це зробити, змішуючи декларативні і імперативні функції безпеки:






(resource “Test Resource” “This is an example resource” “/Test”
(auth-constraints
(role “USERS”)
(if (rogue-ip-list:contains (webserver:ip-address credentials))
(webserver:report-unauthorized)
#f)))

Це дає необмежену гнучкість при програмуванні. Ви можете писати програму декларативно, використовуючи предметно-орієнтоване підмножина мови, і повертатися до повнофункціонального мови програмування, якщо це підмножина не повністю підходить під ваші вимоги.


Висновок


Метапрограмування широко використовується в програмуванні широкомасштабних проектів. У даній статті я торкнувся інструментальних засобів, необхідних для метапрограмування мовою Scheme, а також навів кілька прикладів. Технологія метапрограмування застосовувалася в кількох прикладних областях:



В Scheme ви можете використовувати можливості макромови для визначення практично будь-якого типу предметно-орієнтованої мови. Засіб для цього існує. Просто потрібно вирішити, які можливості реалізуються легше і більш зрозуміло за допомогою макророзширення, а які – за допомогою звичайного коду.

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*