Другой, более продвинутый способ получения 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 для некоторых типов макросов, о которых мы говорили в этом разделе, чтобы полностью понять, какие методы доступны.
Прежде чем мы перейдем к следующему разделу, давайте применим все, что мы узнали, для воссоздания макроса property
стандартной библиотеки.
Воссоздание макроса property
Обычно макрос property
принимает экземпляр TypeDeclaration
, который представляет имя, тип и значение по умолчанию, если таковое имеется, переменной экземпляра. Макрос использует это определение для создания переменной экземпляра, а также методов получения и установки для нее.