# Глава 9. Шаблоны Что происходит, когда происходит вызовов `putPixel` для координат, выходящих за границы экрана? Это зависит от реализации библиотеки пикселей, но возможны три варианта: - 1 Ничего. - 2 Возникает исключение. - 3 Программа выходит из строя. Чтобы «ничего не делать», можно воспользоваться механизмом досрочного возврата. ```nim const ScreenWidth = 1024 # 1 ScreenHeight = 768 proc safePutPixel(x, y: int; col: Color) = if x < 0 or x >= ScreenWidth or y < 0 or y >= ScreenHeight: return # 2 putPixel(x, y, col) # 3 ``` - 1 Для простоты предположим, что разрешение экрана составляет 1024x768. С помощью `const` можно объявлять константы. Константа похожа на переменную, но её значение нельзя изменить, оно должно быть задано во время компиляции. Преимущества этих ограничений будут приведены позже. - 2 Если координаты выходят за пределы допустимого диапазона, вернуть их. - 3 В противном случае вызвать процедуру `putPixel`. Фрагмент программы, подобный `if not inBounds(...): return`, часто встречается в графическом программировании, поэтому может возникнуть желание вынести его в вспомогательную процедуру. К сожалению, оператор `return` завершает текущую процедуру, поэтому следующий код не дает желаемого результата: ```nim proc boundsCheck(x, y: int) = if x < 0 or x >= ScreenWidth or y < 0 or y >= ScreenHeight: return # 1 proc safePutPixel(x, y: int; col: Color) = boundsCheck(x, y) putPixel(x, y, col) ``` - 1 Оператор `return` возвращает `boundsCheck`, но не `safePutPixel`! В языке **Nim** есть конструкция с нужной семантикой «встраивания»: шаблон синтаксически очень похож на процедуру, но вызов шаблона означает раскрытие его тела в месте вызова. ```nim template boundsCheck(a, b: int) = # 1 if a < 0 or a >= ScreenWidth or b < 0 or b >= ScreenHeight: return # 2 proc safePutPixel(x, y: int; col: Color) = boundsCheck(x, y) # 3 putPixel(x, y, col) ``` - 1 Объявлен шаблон с именем `boundsCheck` с параметрами `a` и `b` типа `int`. - 2 `return` внутри шаблона означает возврат из вызывающей функции шаблона. - 3 Шаблон можно вызвать так же, как и процедуру. Несмотря на то, что `boundsCheck(x, y)` выглядит как вызов, на самом деле он не вызывается, а тело `boundsCheck` вставляется непосредственно в `safePutPixel`. При вставке также происходит подстановка параметров: в нашем примере параметр шаблона a заменяется параметром процедуры `x`, а `b` — на `y`. Шаблон — это простая форма макроса, которая чаще всего используется для абстракции потока управления. В шаблон можно легко передать несколько операторов: ```nim template wrap(body: untyped) = # 1 drawText 0, 10, "Before Body", 8, Yellow # 2 body # 3 wrap: # 4 for i in 1..3: let textToDraw = "Добро пожаловать в Nim " & $i & " раз!" drawText 10, i*10, textToDraw, 8, Yellow ``` - 1 Шаблон `wrap` принимает список операторов под названием `body`. Тип `untyped` будет описан позже - 2 Вызов `drawText` выполняется - 3 перед выполнением операторов, передаваемых через `body` - 4 С помощью синтаксической обёртки: (обратите внимание на двоеточие), за которой следует цикл `for` с отступом, мы передаём цикл `for` в шаблон обёртки. Несмотря на то, что шаблоны основаны на концептуально довольно простом механизме подстановки, который полностью выполняется во время компиляции, их возможности поражают. При наличии некоторого опыта они позволяют использовать стиль программирования, который позволяет абстрагироваться от многих деталей, что приводит к сокращению кода без ущерба для читабельности. В качестве примера мы вводим окружение `withColor`. Внутри этого окружения `putPixel` и `drawText` должны неявно использовать указанный цвет, чтобы нам не приходилось повторять аргумент `color` снова и снова. Мы объявляем варианты `putPixel` и `drawText` как шаблоны, использующие необъявленную переменную `colorContext`: ```nim template putPixel(x, y: int) = putPixel(x, y, colorContext) # 1 template drawText(x, y: int; s: string) = drawText(x, y, s, colorContext) # 2 ``` - 1 Шаблон `putPixel`, который не принимает цвет, делегирует свою работу существующей процедуре `putPixel`, используя все еще не объявленный параметр цвета `colorContext`. - 2 Аналогичным образом действует `drawText`. Несмотря на то, что `putPixel` и `drawText` уже находятся в нашей области видимости, допустимо _снова_ использовать те же имена для _разных_ (но в данном случае связанных) операций. Это устраняет неоднозначность вызовов. В нашем случае устранить неоднозначность просто: вызов `putPixel(x, y, color)` разрешается в `pixels.putPixel(x, y, color)`, а вызов без параметра `color` разрешается во вновь созданный шаблон с таким же именем. То же самое относится к `drawText`. Шаблоны могут легко ссылаться на необъявленные сущности, поскольку только при раскрытии шаблона результат проверяется на соответствие семантике. Переменная `colorContext` объявлена внутри окружения `withColor`. Она помечена как `inject`, чтобы быть видимой внутри тела функции: ```nim template withColor(col: Color; body: untyped) = # 1 let colorContext {.inject.} = col # 2 body withColor Blue: # 3 putPixel 3, 4 # 4 drawText 10, 10, "abc", 12 ``` - 1 `withColor` — это шаблон, который принимает на вход `col` и `body` кода. - 2 `colorContext` внедряется в `body`. Без аннотации `.inject` -- `putPixel` и `drawText` _не смогли бы_ увидеть переменную `colorContext` . - 3 `blue` передается в `col`, а раздел кода `putPixel ... drawText ...` — в `body` с помощью синтаксиса с двоеточием. - 4 Вызываются шаблоны `putPixel` и `drawText` . После раскрытия всех шаблонов полный пример выглядит так: ```nim let colorContext = Blue putPixel(3, 4, colorContext) drawText(10, 10, "abc", colorContext) ```