Templates: подстановка кода на этапе компиляции

Разбираемся, как template превращает часто повторяющийся код в удобный «ярлык», который компилятор сам подставит в нужное место.

template — конструкция Nim, которая при компиляции просто подставляет своё тело в место вызова, как будто вы вручную скопировали код туда.

Зачем нужен ещё один способ оформить код

В Nim уже есть привычный способ переиспользовать код — процедуры (proc). Казалось бы, зачем ещё что-то? Дело в том, что у обычной процедуры есть накладные расходы: вызов процедуры — это отдельная операция, у неё есть свои параметры со строгими типами, и иногда компилятору приходится создавать промежуточные значения. Для простых вещей вроде «сравни два числа и верни большее» это несущественно, но для кода, который выполняется миллионы раз в цикле, или для кода, который должен работать с любым типом без явного дублирования, хочется чего-то более лёгкого.

template — это именно такой лёгкий инструмент. Он не создаёт настоящий вызов функции во время работы программы. Вместо этого ещё на этапе компиляции компилятор берёт тело шаблона и буквально подставляет его текст в то место, где шаблон был вызван — как будто вы взяли фрагмент кода и вставили его через copy-paste, только автоматически и без опечаток. После такой подстановки компилятор компилирует уже готовый развёрнутый код, как если бы вы с самого начала написали его вручную.

Простой пример: шаблон вместо процедуры

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

template myMax(a, b: untyped): untyped =
  if a > b: a else: b

let x = 5
let y = 12
echo myMax(x, y)

Вывод:

12

Разбираем построчно

Строка template myMax(a, b: untyped): untyped = объявляет шаблон с именем myMax и двумя параметрами — a и b. Обратите внимание на тип untyped: это не «неизвестный» тип в смысле динамической типизации, а специальная пометка «сюда подойдёт вообще любое валидное выражение Nim — число, переменная, вызов функции — компилятор не будет проверять его тип заранее, а разберётся уже после подстановки». Это одно из главных отличий шаблона от обычной процедуры: в процедуре вы обязаны заранее указать конкретный тип параметра (например, int), а untyped-параметр шаблона годится для чего угодно.

Тело шаблона — это обычное выражение if a > b: a else: b, вычисляющее большее из двух значений. Когда компилятор встречает вызов myMax(x, y), он не выполняет «вызов функции» в привычном смысле — он берёт тело шаблона и подставляет вместо a и b переданные аргументы x и y, получая на этом месте фактически строку if x > y: x else: y. Именно этот развёрнутый код и компилируется дальше, без единого лишнего вызова во время работы программы.

Чем это отличается от обычной процедуры

Если бы myMax была обычной процедурой proc myMax(a, b: int): int, компилятор создал бы настоящий вызов: сохранил бы аргументы, передал управление в тело процедуры, дождался возврата значения. С шаблоном ничего этого не происходит — итоговый скомпилированный код выглядит так, будто вы вообще не выделяли myMax в отдельную единицу, а написали if x > y: x else: y прямо в месте использования. Это называется «инлайнинг» (inlining), и шаблоны в Nim — самый прямой способ получить его гарантированно, без надежды на то, что компилятор сам решит развернуть вызов ради оптимизации.

Есть и второе важное отличие, помимо скорости: благодаря untyped-параметрам шаблон можно вызвать с чем угодно — с переменной, с литералом, даже с целым блоком кода — и это будет работать без явного дублирования шаблона под разные типы, как пришлось бы делать с обычными процедурами для строго разных типов данных.

Мини-пример: шаблон-«секундомер»

Шаблоны отлично подходят для ситуаций, когда один и тот же «обвес» нужно применить вокруг разного кода — например, замер времени выполнения блока. Здесь удобство untyped-параметра раскрывается в полную силу: в шаблон можно передать целый блок кода как единственный аргумент.

import std/times

template timeIt(label: string, body: untyped) =
  let start = cpuTime()
  body
  let elapsed = cpuTime() - start
  echo label, ": ", elapsed, " sec"

timeIt("Podschet summy"):
  var summa = 0
  for i in 1..1000:
    summa += i
  echo "Summa = ", summa

Вывод:

Summa = 500500
Podschet summy: 0.0001234 sec

Обратите внимание: параметр body — это не просто число или строка, а целый блок кода (цикл с подсчётом суммы), переданный в шаблон как аргумент благодаря специальному синтаксису двоеточия после вызова. Внутри шаблона слово body подставляется на то место, где оно упомянуто — между замером стартового времени start и вычислением elapsed. Именно поэтому вывод «Summa = 500500» появляется раньше строки с замером времени: сначала выполняется подставленный блок, и только потом — код шаблона, идущий после body.

Частые ошибки новичков

Первая ошибка — забыть, что параметр untyped не вычисляется один раз перед подстановкой, а копируется как есть в каждое место, где он упомянут в теле шаблона. Если в шаблоне переменная a используется дважды (как в нашем myMax, где a встречается и в сравнении, и в ветке if), а вызывающий код передал в качестве аргумента не простую переменную, а выражение с побочным эффектом — например, вызов процедуры, которая печатает что-то на экран, — этот побочный эффект произойдёт не один раз, а столько раз, сколько параметр упомянут в теле. Начинающие часто не ожидают такого «двойного срабатывания».

Вторая ошибка — путать шаблон с обычной процедурой и ожидать от него строгой проверки типов при объявлении. Раз параметр помечен untyped, компилятор не станет ругаться на этапе объявления шаблона, если внутри тела есть операция, которая не подойдёт для какого-то конкретного типа — ошибка вылезет только в момент конкретного вызова, что иногда сбивает с толку при отладке.

Третья ошибка — переусердствовать с шаблонами там, где хватило бы обычной процедуры. Если код не требует инлайнинга ради скорости и не нуждается в передаче «сырого» непроверенного кода, обычная proc с явными типами читается понятнее и даёт более внятные ошибки компиляции. Шаблоны — специализированный инструмент, а не замена процедур по умолчанию.

Итоги

  • Шаблон template на этапе компиляции подставляет своё тело прямо в место вызова — без накладных расходов на настоящий вызов функции.
  • Тип untyped у параметра означает «подойдёт любое валидное выражение Nim», проверка типов откладывается до момента конкретной подстановки.
  • В шаблон можно передать не только значение, но и целый блок кода — это делает шаблоны удобными для «обвязок» вроде замера времени или логирования.
  • Главная опасность — параметр, упомянутый в теле несколько раз, подставляется целиком каждый раз, и если у аргумента есть побочный эффект, он сработает многократно.
Проверьте себя
1. Что делает компилятор Nim, когда встречает вызов template во время компиляции?
AСоздаёт полноценный вызов функции, как для обычной proc
BПодставляет тело шаблона прямо в место вызова, заменяя параметры на переданные аргументы
CОткладывает выполнение шаблона до запуска программы
DКомпилирует шаблон в отдельный поток выполнения
2. Почему в шаблоне myMax с параметрами типа untyped опасно, если аргумент с побочным эффектом (например, вызов процедуры с echo внутри) передаётся в параметр, упомянутый в теле шаблона дважды?
AЭто не опасно, untyped-параметры вычисляются один раз перед подстановкой
BАргумент подставляется как текст в каждое место упоминания, поэтому побочный эффект сработает несколько раз подряд
CКомпилятор автоматически исправит это и вычислит аргумент один раз
DТакой код вообще не скомпилируется