Выбрать главу

В предыдущих главах мы в основном использовали макросы внутри самих типов и методов для доступа к информации времени компиляции или чтения аннотаций. Однако это значительно снижает эффективность макросов, поскольку они могут динамически реагировать на добавление или аннотирование новых типов. Следующая концепция метапрограммирования Crystal, которую мы собираемся рассмотреть, — это интроспекция типов во время компиляции, которая будет охватывать следующие темы:

• Итерация переменных типа

• Итерационные типы

• Итерационные методы

К концу этой главы вы сможете создавать макросы, которые генерируют код, используя переменные экземпляра, методы и/или информацию о типе, а также данные, считываемые из аннотаций.

Технические требования

Требования к этой главе следующие:

• Рабочая установка Кристалла.

Инструкции по настройке Crystal можно найти в Главе 1 «Введение в Crystal».

Все примеры кода, использованные в этой главе, можно найти в папке Главы 12 на GitHub: https://github.com/PacktPublishing/Crystal-Programming/tree/main/Chapter12.

Итерация переменных типа

Одним из наиболее распространенных случаев использования интроспекции типов является перебор переменных экземпляра типа. Простейшим примером этого может быть добавление метода #to_h к объекту, который возвращает хэш, используя переменные экземпляра типа для ключа/значений. Это будет выглядеть так:

class Foo

  getter id : Int32 = 1

  getter name : String = "Jim"

  getter? active : Bool = true

  def to_h

    {

      "id" => @id,

      "name" => @name,

      "active" => @active,

    }

  end

end

pp Foo.new.to_h

Который, когда будет выполнен, выведет следующее:

{"id" => 1, "name" => "Jim", "active" => true}

Однако это далеко не идеально, поскольку вам нужно не забывать обновлять этот метод каждый раз, когда добавляется или удаляется переменная экземпляра. Он также не обрабатывает случай, когда этот класс расширяется и добавляются дополнительные переменные экземпляра.

Мы могли бы улучшить его, используя макрос для перебора переменных экземпляра этого типа с целью построения хеша. Новый метод #to_h будет выглядеть так:

def to_h

  {% begin %}

    {

      {% for ivar in @type.instance_vars %}

        {{ivar.stringify}} => @{{ivar}},

      {% end %}

    }

  {% end %}

end

Если вы помните из Главы 10 «Работа с макросами», нам нужно обернуть эту логику в начало/конец, чтобы сделать все допустимым синтаксисом Crystal. Затем мы используем метод #instance_vars для экземпляра TypeNode, полученного с помощью специальной макропеременной @type. Этот метод возвращает Array(MetaVar), который включает информацию о каждой переменной экземпляра, такую как ее имя, тип и значение по умолчанию.

Наконец, мы перебираем каждую переменную экземпляра с помощью цикла for, используя строковое представление имени переменной экземпляра в качестве ключа и, конечно же, ее значение в качестве значения хеша. Запуск этой версии программы дает тот же результат, что и раньше, но с двумя основными преимуществами:

• Он автоматически обрабатывает вновь добавленные/удаленные переменные экземпляра.

• Он будет включать переменные экземпляра, определенные для дочерних типов, поскольку макрос расширяется для каждого конкретного подкласса, поскольку он использует макропеременную @type.

Подобно итерации переменных экземпляра, доступ к переменным класса также можно получить с помощью метода TypeNode#class_vars. Однако есть одна серьезная ошибка при переборе переменных экземпляра/класса типа.

ПРЕДУПРЕЖДЕНИЕ

Доступ к переменным экземпляра возможен только в контексте метода. Попытка сделать это вне метода всегда приведет к получению пустого массива, даже если используется в ловушке завершения макроса.

По сути, это ограничение компилятора Crystal на данный момент, которое может быть реализовано в той или иной форме в будущем. Но до тех пор лучше иметь это в виду, чтобы не тратить время на отладку чего-то, что просто не будет работать. Посетите https://github.com/crystal-lang/crystal/issues/7504 для получения дополнительной информации об этом ограничении.

Другой вариант использования итерации переменных экземпляра — это добавление переменных экземпляра к некоторой внешней логике, которая может быть включена в модуль. Например, предположим, что у нас есть модуль Incrementable, который определяет один метод #increment, который, как следует из названия, будет увеличивать определенные выбранные переменные. Реализация этого метода может использовать @type.instance_vars вместе с ArrayLiteral#select, чтобы определить, какие переменные следует увеличить.