ch09.md 8.8 KB

Глава 9. Шаблоны

Что происходит, когда происходит вызывов putPixel для координат, выходящих за границы экрана? Это зависит от реализации библиотеки пикселей, но возможны три варианта:

  • 1 Ничего.
  • 2 Возникает исключение.
  • 3 Программа выходит из строя.

Чтобы «ничего не делать», можно воспользоваться механизмом досрочного возврата.

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 завершает текущую процедуру, поэтому следующий код не дает желаемого результата:

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

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.

Шаблон — это простая форма макроса, которая чаще всего используется для абстракции потока управления. В шаблон можно легко передать несколько операторов:

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:

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, чтобы быть видимой внутри тела функции:

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 .

После раскрытия всех шаблонов полный пример выглядит так:

let colorContext = Blue
putPixel(3, 4, colorContext)
drawText(10, 10, "abc", colorContext)