ch21.md 14 KB

Chapter 21. Procedures

A routine is a symbol of kind: proc, func, method, iterator, macro, template, converter. What most programming languages call methods or functions are called procedures in Nim. A procedure declaration consists of an identifier, zero or more formal parameters, a return value type and a block of code. Formal parameters are declared as a list of identifiers separated by either comma or semicolon. A parameter is given a type by : typename. The type applies to all parameters immediately before it, until either the beginning of the parameter list, a semicolon separator, or an already typed parameter, is reached. The semicolon can be used to make separation of types and subsequent identifiers more distinct. # Using only commas proc foo(a, b: int, c, d: bool): int # Using semicolon for visual distinction proc foo(a, b: int; c, d: bool): int # Will fail: a is untyped since ';' stops type propagation. proc foo(a; b: int; c, d: bool): int A parameter may be declared with a default value which is used if the caller does not provide a value for the argument. # b is optional with 47 as its default value proc foo(a: int, b: int = 47): int Parameters can be declared as mutable and so allow the proc to modify the corresponding arguments, by using the type modifier var.

                                                                     145

# "returning" a value to the caller through the 2nd argument # Notice that the function uses no actual return value; its return type is void proc foo(inp: int, outp: var int) =

outp = inp + 47

If the proc declaration has no body, it is a forward declaration. If the proc returns a value, the procedure body can access an implicitly declared variable named result that represents the return value. Procs can be overloaded. The overloading resolution algorithm determines which proc is the best match for the arguments. Example: proc toLower(c: char): char = # toLower for characters

if c in {'A'..'Z'}:
  result = chr(ord(c) + (ord('a') - ord('A')))
else:
  result = c

proc toLower(s: string): string = # toLower for strings

result = newString(len(s))
for i in 0..len(s) - 1:
  result[i] = toLower(s[i]) # calls toLower for characters; no recursion!

Calling a procedure can be done in many different ways: proc callme(x, y: int, s: string = "", c: char, b: bool = false) = discard # call with positional arguments # parameter bindings: callme(0, 1, "abc", '\t', true) # (x=0, y=1, s="abc", c='\t', b=true) # call with named and positional arguments: callme(y=1, x=0, "abd", '\t') # (x=0, y=1, s="abd", c='\t', b=false) # call with named arguments (order is not relevant): callme(c='\t', y=1, x=0) # (x=0, y=1, s="", c='\t', b=false) # call as a command statement: no () needed: callme 0, 1, "abc", '\t' # (x=0, y=1, s="abc", c='\t', b=false) A procedure may call itself recursively. Operators are procedures with a special operator symbol as identifier: proc $(x: int): string =

# converts an integer to a string; this is a prefix operator.
result = intToStr(x)

146 Operators with one parameter are prefix operators, operators with two parameters are infix operators. (However, the parser distinguishes these from the operator’s position within an expression.) There is no way to declare postfix operators: all postfix operators are built-in and handled by the grammar explicitly. Any operator can be called like an ordinary proc with the opr\ notation. (Thus an operator can have more than two parameters): proc *+(a, b, c: int): int =

# Multiply and add
result = a * b + c

assert *+(3, 4, 6) == +(*(a, b), c) 21.1. Method call syntax The syntax obj.methodName(args) can be used instead of methodName(obj, args). The parentheses can be omitted if there are no remaining arguments: obj.len (instead of len(obj)). This method call syntax is not restricted to objects, it can be used to supply any type of first argument for routines: echo "abc".len # is the same as echo len "abc" echo "abc".toUpper() echo {'a', 'b', 'c'}.card stdout.writeLine("Hallo") # the same as writeLine(stdout, "Hallo") Another way to look at the method call syntax is that it provides the missing postfix notation. The method call syntax conflicts with explicit generic instantiations: pT cannot be written as x.p[T] because x.p[T] is always parsed as (x.p)[T]. See also: Section 27.17, “Method call syntax limitations”. The [: ] notation was designed to mitigate this issue: x.p[:T] is rewritten by the parser to pT, x.p:T is rewritten to pT. Note that [: ] has no AST representation, the rewrite is performed directly in the parsing step.

                                                                        147

21.2. Properties Nim has no need for get-properties: Ordinary get-procedures that are called with the method call syntax achieve the same. But setting a value is different; for this we need a special setter syntax: # Module asocket type

Socket* = ref object of RootObj
   host: int # cannot be accessed from the outside of the module

proc host=*(s: var Socket, value: int) {.inline.} =

## setter of hostAddr.
## This accesses the 'host' field and is not a recursive call to
## `host=` because the builtin dot access is preferred if it is
## available:
s.host = value

proc host*(s: Socket): int {.inline.} =

## getter of hostAddr
## This accesses the 'host' field and is not a recursive call to
## `host` because the builtin dot access is preferred if it is
## available:
s.host

# module B import asocket var s: Socket new s s.host = 34 # same as host=(s, 34) A proc defined as f= (with the trailing =) is called a setter. A setter can be called explicitly via the common back ticks notation: proc f=(x: MyObject; value: string) =

discard

f=(myObject, "value") f= can be called implicitly in the pattern x.f = value if and only if the type of x does not have a field named f or if f is not visible in the current module. These rules ensure that object fields and accessors can have the same name. Within the module x.f is then always interpreted as field access and outside the module it is interpreted as an accessor proc call. 148 21.3. Indexing Array-like access properties (a[i]) are supported too as the [] subscript operator can be overloaded. To offer both read and write access no less than 3 versions have to be provided: proc [](x: Container; i: Index): ElementType proc [](x: var Container; i: Index): var ElementType proc []=(x: Container; i: Index; newValue: ElementType) For example: type

Matrix* = object
  # Array for internal storage of elements.
   data: ptr UncheckedArray[float]
  # Row and column dimensions.
   m*, n*: int

proc []*(m: Matrix, i, j: int): float {.inline.} =

## Get a single element.
m.data[i * m.n + j]

proc []*(m: var Matrix, i, j: int): var float {.inline.} =

## Get a single element.
m.data[i * m.n + j]

proc []=*(m: var Matrix, i, j: int, s: float) =

## Set a single element.
m.data[i * m.n + j] = s

21.4. Command invocation syntax Routines can be invoked without the () if the call is syntactically a statement. This command invocation syntax also works for expressions, but then only a single argument may follow. This restriction means echo f 1, f 2 is parsed as echo(f(1), f(2)) and not as echo(f(1, f(2))). The method call syntax may be used to provide one more argument in this case: proc optarg(x: int, y: int = 0): int = x + y proc singlearg(x: int): int = 20*x echo optarg 1, " ", singlearg 2 # prints "1 40"

                                                                        149

let fail = optarg 1, optarg 8 # Wrong. Too many arguments for a command call let x = optarg(1, optarg 8) # traditional procedure call with 2 arguments let y = 1.optarg optarg 8 # same thing as above, w/o the parenthesis assert x == y The command invocation syntax also cannot have complex expressions as arguments. For example: (Section 21.6, “Anonymous procs”), if, case or try. Function calls with no arguments still need ()` to distinguish between a call and the function itself as a first-class value. 21.5. Closures Procedures can appear at the top level in a module as well as inside other scopes, in which case they are called nested procs. A nested proc can access local variables from its enclosing scope and if it does so it becomes a closure. Any captured variables are stored in a hidden additional argument to the closure (its environment) and they are accessed by reference by both the closure and its enclosing scope (i.e. any modifications made to them are visible in both places): proc outer =

var i = 0
proc mutate = 1
   inc i
mutate()
echo i 2

outer() 1 mutate accesses i which belongs to outer. This access causes mutate to

have the calling convention .closure.

2 Outputs 1. 21.5.1. Creating closures in loops Since closures capture local variables by reference it is often not the wanted behavior inside loop bodies: 150 var later: seq[proc ()] = @[] for i in 1..2: later.add proc () = echo i for x in later: x() # Produces: 2 2 In order to capture a loop variable "by value", both a helper routine and an additional local variable (which is then captured instead of the for loop variable) have to be used: var later: seq[proc ()] = @[] for i in 1..2:

  (proc =
    let j = i
    later.add proc () = echo j)()

for x in later: x() # Produces: 1 2 The standard library offers the helpers system.closureScope and sugar.capture for syntactic shortcuts that accomplish the same. 21.6. Anonymous procs Unnamed procedures can be used as lambda expressions and be passed to other routines: var cities = @["Frankfurt", "Tokyo", "New York", "Kyiv"] cities.sort(proc (x,y: string): int = cmp(x.len, y.len)) Procs as expressions can appear both as nested procs and inside top-level executable code. The sugar module contains a => macro which enables a more succinct syntax for anonymous procedures resembling lambdas as they are in languages like JavaScript, C#, etc. 21.7. Func The func keyword introduces a shortcut for a noSideEffect proc.

                                                                     151

func binarySearchT: int Is short for: proc binarySearchT: int {.noSideEffect.} See Section 26.4, “Side effects” for further information. 21.8. Non-overloadable built-ins The following built-in procs cannot be overloaded for reasons of implementation simplicity (they require specialized semantic checking): declared, defined, definedInScope, compiles, sizeof, is, shallowCopy, getAst, astToStr, spawn, procCall Thus they act more like keywords than like ordinary identifiers; unlike a keyword however, a redefinition may shadow the definition in the system module. From this list the following should not be written in dot notation x.f since x cannot be type-checked before it gets passed to f: declared, defined, definedInScope, compiles, getAst, astToStr 21.9. Var parameters The type of a parameter may be prefixed with the var keyword: proc divmod(a, b: int; res, remainder: var int) =

res = a div b
remainder = a mod b

var

x, y: int

divmod(8, 5, x, y) # modifies x and y assert x == 1 assert y == 3 In the example, res and remainder are var parameters. Var parameters can be 152 modified by the procedure and the changes are visible to the caller. The argument passed to a var parameter has to be an l-value. Var parameters are implemented as hidden pointers. The above example is comparable to: proc divmod(a, b: int; res, remainder: ptr int) =

res[] = a div b
remainder[] = a mod b

var

x, y: int

divmod(8, 5, addr(x), addr(y)) assert x == 1 assert y == 3 In the examples, var parameters or pointers are used to provide two return values. This can be done in a cleaner way by returning a tuple: proc divmod(a, b: int): tuple[res, remainder: int] =

(a div b, a mod b)

var t = divmod(8, 5) assert t.res == 1 assert t.remainder == 3 One can use tuple unpacking to access the tuple’s fields: var (x, y) = divmod(8, 5) # tuple unpacking assert x == 1 assert y == 3

          var parameters are never necessary for efficient parameter

          passing. Since non-var parameters cannot be modified, a Nim
          compiler is always free to pass arguments by reference if it
          considers it can speed up execution.

21.10. Var return type A routine that is not a template nor a macro may return a var type which means that the returned value is an l-value and can be modified by the caller: var g = 0

                                                                     153

proc writeAccessToG(): var int =

 result = g

writeAccessToG() = 6 assert g == 6 It is a static error if the implicitly introduced pointer could be used to access a location beyond its lifetime: proc writeAccessToG(): var int =

 var g = 0
 result = g # Error!

For iterators, a component of a tuple return type can have a var type too: iterator mpairs(a: var seq[string]): tuple[key: int, val: var string] =

 for i in 0..a.high:
   yield (i, a[i])

In the standard library every name of a routine that returns a var type starts with the prefix m per convention. Memory safety for returning by var T is ensured by a simple borrowing rule: If result does not refer to a location pointing to the heap (that is in result = X the X involves a ptr or ref access) then it has to be derived from the routine’s first parameter: proc forwardT: var T =

 result = x # ok, derived from the first parameter.

proc p(param: var int): var int =

 var x: int
 # we know 'forward' provides a view into the location derived from
 # its first argument 'x'.
 result = forward(x) # Error: location is derived from `x`
                       # which is not p's first parameter and lives
                       # on the stack.

In other words, the lifetime of what result points to is attached to the lifetime of the first parameter and that is enough knowledge to verify memory safety at the call site.