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», проверка типов откладывается до момента конкретной подстановки. - В шаблон можно передать не только значение, но и целый блок кода — это делает шаблоны удобными для «обвязок» вроде замера времени или логирования.
- Главная опасность — параметр, упомянутый в теле несколько раз, подставляется целиком каждый раз, и если у аргумента есть побочный эффект, он сработает многократно.