• Использование аннотаций для влияния на логику времени выполнения.
• Представление данных аннотаций/типов во время выполнения.
• Определение значения константы во время компиляции.
• Создание собственных ошибок времени компиляции.
К концу этой главы вы должны иметь более глубокое понимание метапрограммирования в Crystal. У вас также должны быть некоторые идеи о неочевидных вариантах использования метапрограммирования, которые позволят вам создавать уникальные решения проблем в вашем приложении.
Технические требования
Прежде чем мы углубимся в эту главу, в вашей системе должно быть установлено следующее:
• Рабочая установка Crystal.
Инструкции по настройке Crystal можно найти в
Все примеры кода, использованные в этой главе, можно найти в папке
Использование аннотаций для влияния на логику времени выполнения
Как мы узнали в
В некоторых случаях вам может потребоваться реализовать функцию с использованием аннотаций для настройки чего-либо, но логика, требующая этих данных, не может быть сгенерирована только с помощью макросов и должна выполняться во время выполнения. Например, предположим, что мы хотим иметь возможность печатать экземпляры объектов в различных форматах. Эта логика может использовать аннотации, чтобы отметить, какие переменные экземпляра следует предоставлять, а также настроить способ их форматирования. Высокоуровневый пример этого будет выглядеть так:
annotation Print; end
class MyClass
include Printable
@[Print]
property name : String = "Jim"
@[Print(format: "%F")]
property created_at : Time = Time.utc
@[Print(scale: 1)]
property weight : Float32 = 56.789
end
MyClass.new.print
Результатом этого может быть следующее:
---
name: Jim
created_at: 2021-11-16
weight: 56.8
---
Чтобы реализовать это, логика печати должна иметь доступ как к данным аннотации, так и к значению переменной экземпляра, которая должна быть напечатана. В нашем случае модуль Printable
позаботится об этом, определяя метод, который обрабатывает итерацию и печатает каждую применимую переменную экземпляра. В конечном итоге это будет выглядеть так:
module Printable
def print(printer)
printer.start
{% for ivar in @type.instance_vars.select(&.annotation Print) %}
printer.ivar({{ivar.name.stringify}},
@{{ivar.name.id}},
{{ivar.annotation(Print).named_args.double_splat}})
{% end %}
printer.finish
end
def print(io : IO = STDOUT)
print IOPrinter.new(io)
end
end
Большая часть логики выполняется в методе #print(printer)
. Этот метод напечатает начальный шаблон, которым в данном случае являются три тире. Затем он использует макрос цикла for
для перебора переменных экземпляра включающего типа. Переменные экземпляра фильтруются таким образом, что включаются только те, у которых есть аннотация Print
. Затем для каждой из этих переменных вызывается метод #ivar
на принтере с именем и значением переменной экземпляра, а также любых именованных аргументов, определенных в аннотации. Наконец, он печатает конечный образец, который также состоит из трех тире.
Для поддержки предоставления значений из аннотации мы также используем метод NamedTupleLiteral#double_splat
вместе с Annotation#named_ args
. Эта комбинация предоставит любые пары ключ/значение, определенные в аннотации, в качестве именованных аргументов для вызова метода.
Метод #print(io
) служит основной точкой входа для печати экземпляра. Он позволяет предоставить пользовательский I/O, на который должны выводиться данные, но по умолчанию это STDOUT
. I/O используется для создания другого типа, который фактически выполняет печать:
struct IOPrinter
def initialize(@io : IO); end
def start
@io.puts "---"
end
def finish
@io.puts "---"
@io.puts
end