Шрифт:
Интервал:
Закладка:
COMMIT_SHA = {{ env("BUILD_SHA_HASH") || "" }}
pp COMMIT_SHA
При запуске этого кода обычно печатается пустая строка, а при установке связанной переменной env выводится это значение. Установка этого значения через переменную env, а не генерация внутри самого макроса с помощью системного вызова, гораздо более переносима, поскольку не зависит от Git, а также гораздо проще интегрируется с внешними системами сборки, такими как Make.
Одним из ограничений макросов является то, что сгенерированный из макроса код также должен быть действительным кодом Crystal, как показано здесь:
def {{"foo".id}}
"foo"
end
Этот предыдущий код не является допустимой программой, поскольку метод неполный и не полностью определен в макросе. Этот метод можно включить в макрос, обернув все тегами {% begin %}/{% end %}, которые будут выглядеть следующим образом:
{% begin %}
def {{"foo".id}}
"foo"
end
{% end %}
На этом этапе вы должны иметь четкое начальное представление о том, что такое макросы, как их определять и для каких случаев использования они предназначены, что позволит вам сохранить ваш код СУХИМ (DRY). Далее мы рассмотрим API макросов, чтобы можно было создавать более сложные макросы.
Понимание API макросов
В примерах из предыдущего раздела в контексте макроса использовались различные переменные разных типов, такие как числа, которые мы перебираем, строки, которые мы используем для создания идентификаторов, и логические значения, которые мы сравниваем для условной генерации кода. Было бы легко предположить, что это напрямую соответствует стандартным типам Number, String и Bool. Однако это не так. Как мы упоминали в разделе «Определение макросов» этой главы, макросы работают на узлах AST и, как таковые, имеют свой собственный набор типов, похожий на связанные с ними обычные типы Crystal, но с подмножеством API. Например, типы, с которыми мы до сих пор работали, включают NumberLiteral, StringLiteral и BoolLiteral.
Все типы макросов находятся в пространстве имен Crystal::Macros в документации API, которая находится по адресу https://crystal-lang.org/api/Crystal/Macros.html. К наиболее распространенным/полезным типам относятся следующие:
• Def: описывает определение метода.
• TypeNode: описывает тип (класс, структура, модуль, библиотека).
• MetaVar: описывает переменную экземпляра.
• Arg: описывает аргумент метода.
•Annotation: представляет аннотацию, применяемую к типу, методу или переменной экземпляра (подробнее об этом в следующей главе).
Crystal предоставляет удобный способ получить экземпляр первых двух типов в виде макропеременных @def и @type. Как следует из их названий, использование @def внутри метода вернет экземпляр Def, представляющий этот метод. Аналогично, использование @type вернет экземпляр TypeNode для связанного типа. Доступ к другим типам можно получить через методы, основанные на одном из этих двух типов. Например, запуск следующей программы выведет "Метод hello внутри Foo":
class Foo
def hello
{{"The #{@def.name} method within #{@type.name}"}}
end
end
pp Foo.new.hello
Другой, более продвинутый способ получения TypeNode — использование макрометода parse_type. Этот метод принимает StringLiteral, который может быть создан динамически, и возвращает один из нескольких типов макросов в зависимости от того, что представляет собой строка. Дополнительную информацию см. в документации по методу https://crystal-lang.org/api/Crystal/Macros.html.
Как мы упоминали ранее, API макросов позволяет нам вызывать фиксированное подмножество обычных методов API для литеральных типов. Другими словами, это позволяет нам вызывать ArrayLiteral#select, но не ArrayLiteral#each_repeated_permutation, или StringLiteral#gsub, но не StringLiteral#scan.
В дополнение к этим примитивным типам ранее упомянутые типы макросов предоставляют свой собственный набор методов, чтобы мы могли получать информацию о связанном типе, например:
• Тип возвращаемого значения, его видимость или аргументы метода.
• Тип/значение по умолчанию аргумента метода.
• Какие аргументы объединения/обобщения имеет тип, если таковые имеются.
Конечно, их слишком много, чтобы их здесь упоминать, поэтому я предлагаю просмотреть документацию по API для получения полного списка. А пока давайте применим некоторые из этих методов:
class Foo
def hello(one : Int32, two, there, four : Bool, five :
String?)
{% begin %}
{{"#{@def.name} has #{@def.args.size} arguments"}}
{% typed_arguments = @def.args.select(&.restriction) %}
{{"with #{typed_arguments.size} typed
arguments"}}
{{"and is a #{@def.visibility.id} method"}}
{% end %}
end
end
Foo.new.hello 1, 2, 3, false, nil
Эта программа выведет следующее:
"hello has 5 arguments"
"with 3 typed arguments"
"and is a public method"
Первая строка выводит имя метода и количество его аргументов через ArrayLiteral#size, поскольку Def#args возвращает ArrayLiteral(Arg). Затем мы используем метод ArrayLiteral#select, чтобы получить массив, содержащий только аргументы, имеющие ограничение типа. Arg#restriction возвращает TypeNode на основе типа ограничения или Nop, которое является ложным значением и используется для представления пустого узла. Наконец, мы используем Def#visibility, чтобы узнать уровень видимости метода. Он возвращает символический литерал, поэтому мы вызываем для него #id, чтобы получить его общее представление.
Существует еще одна специальная макропеременная @top_level, которая возвращает TypeNode, представляющий пространство имен верхнего уровня. Если мы не воспользуемся этим, единственный другой способ получить к нему доступ — это вызвать @type в пространстве имен верхнего уровня, что сделает невозможным ссылку на него внутри другого типа. Давайте посмотрим, как можно использовать эту переменную:
A_CONSTANT = 0
module Foo; end
{% if @top_level.has_constant?("A_CONSTANT") && @top_level
.has_constant?("Foo") %}
puts "this is printed"
{% else %}
puts "this is not printed"
{% end %}
В этом примере мы использовали TypeNode#has_constant?, который возвращает BoolLiteral, если связанный TypeNode имеет предоставленную константу, предоставленную в виде StringLiteral, SymbolLiteral или MacroId (тип, который вы получаете при вызове #id для другого типа). Этот метод работает как для реальных констант, так и для типов.
Понимание API макросов имеет решающее значение для написания макросов, использующих информацию, полученную из типа и/или метода. Я настоятельно рекомендую прочитать документацию по API для некоторых типов макросов, о которых мы говорили в этом разделе, чтобы полностью понять, какие методы