Что происходит, когда происходит вызывов putPixel для координат, выходящих за границы экрана? Это зависит от реализации библиотеки пикселей, но возможны три варианта:
Чтобы «ничего не делать», можно воспользоваться механизмом досрочного возврата.
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
const можно объявлять константы. Константа похожа на переменную, но её значение нельзя изменить, оно должно быть задано во время компиляции. Преимущества этих ограничений будут приведены позже.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)
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)
boundsCheck с параметрами a и b типа int.return внутри шаблона означает возврат из вызывающей функции шаблона.Несмотря на то, что 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
wrap принимает список операторов под названием body. Тип untyped будет описан позжеdrawText выполняетсяbodyfor с отступом, мы передаём цикл 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
putPixel, который не принимает цвет, делегирует свою работу существующей процедуре putPixel, используя все еще не объявленный параметр цвета colorContext.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
withColor — это шаблон, который принимает на вход col и body кода.colorContext внедряется в body. Без аннотации .inject -- putPixel и drawText не смогли бы увидеть переменную colorContext .blue передается в col, а раздел кода putPixel ... drawText ... — в body с помощью синтаксиса с двоеточием.putPixel и drawText .После раскрытия всех шаблонов полный пример выглядит так:
let colorContext = Blue
putPixel(3, 4, colorContext)
drawText(10, 10, "abc", colorContext)