# Глава 9. Шаблоны Что происходит, когда мы вызываем `putPixel` для координат, выходящих за границы экрана? Это зависит от реализации нашей библиотеки пикселей, но возможны три варианта: - 1 Ничего. - 2 Возникает исключение. - 3 Программа выходит из строя. In order to do “nothing” early returns can be a handy mechanism: ```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 For simplicity, we assume a screen resolution of 1024x768 here. With const you can declare constants. A constant is comparable to a variable but its value cannot be changed and must be set at compile-time. The benefits of these restrictions will be explained later. - 2 If the coordinates are not within bounds return. - 3 Else call the putPixel proc. A program fragment like if not `inBounds(...)`: return is common in graphics programming and so one can desire to move it into a helper proc. Unfortunately, a return statement leaves the current proc and so code like the following has not the desired effect: ```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 The return statements leaves boundsCheck but not safePutPixel! Nim offers a construct which has the “inlining” semantics that we need: A template is syntactically much like a proc, but an invocation to a template means to expand the template's body at the call site: ```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 A template of name boundsCheck with parameters named a and b of type `int` is declared. - 2 return inside a template means to return from the template`s caller. - 3 A template can be invoked just like a proc. Even though boundsCheck(x, y) looks like a call, it’s not called, instead boundsCheck's body is inserted directly into safePutPixel. This insertion also does parameter substitutions; in our example the template parameter a is replaced by the procs parameter x and likewise is b replaced by y. A template is a simple form of a macro, it is most commonly used for control flow abstractions and one can pass multiple statements to a template easily: ```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 = "Welcome to Nim for the " & $i & "th time!" drawText 10, i*10, textToDraw, 8, Yellow ``` - 1 The wrap templates takes a list of statements called body. The type untyped will be explained later. - 2 The `drawText` call runs - 3 before the statements that are passed via body are run. - 4 Via the syntax wrap: (note the colon) followed by the indented for loop we pass the for loop to the wrap template. Even though templates are based on a conceptually quite simple substitution mechanism that is completely performed at compile-time, their power is surprising. With some experience they enable a programming style that lets us abstract away many details leading to shorter programs without negatively impacting the readability. As an example we introduce a withColor environment. Inside this environment `putPixel` and `drawText` should use a specified color implicitly so that we don’t have to repeat the color argument again and again. We declare variants of `putPixel` and drawText as templates that use an undeclared colorContext variable: ```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 The `putPixel` template which does not take a color delegates its work to the existing putPixel proc using the still undeclared colorContext color. - 2 Likewise does `drawText`. Even though putPixel and `drawText` are already in our scope, it is valid to use the same names again for different (but in this case related) operations. The disambiguate the invocations. In our case the disambiguation is simple: A call `putPixel(x, y, color)` resolves to `pixels.putPixel(x, y, color)`, whereas a call without a color parameter resolves to the newly introduced template of this name. The same applies for `drawText`. Templates can easily refer to undeclared entities because only a template expansion implies that the result is checked for semantics. The `colorContext` variable is declared inside the withColor environment. It is marked with inject so that it is visible inside the body: ```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` is a template that takes both a color and a `body` of code. - 2 `colorContext` is injected into body. Without the `.inject` annotation, `putPixel` and `drawText` would not be able to see the `colorContext` variable. - 3 blue is passed to col and the code section `putPixel` ... `drawText` ... to `body` via the colon syntax. - 4 The `putPixel` and `drawText` templates are invoked. After all templates are expanded the complete example looks like: ```nim let colorContext = Blue putPixel(3, 4, colorContext) drawText(10, 10, "abc", colorContext) ```