Procedures
Procedures
What most programming languages call methods
{.interpreted-text
role="idx"} 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. The value will be reevaluated every time the function is called.
Parameters can be declared mutable and so allow the proc to modify those
arguments, by using the type modifier var
.
# "returning" a value to the caller through the 2nd argument
# Notice that the function uses no actual return value at all (ie void)
proc foo(inp: int, outp: var int) =
outp = inp + 47
If the proc declaration has no body, it is a forward
{.interpreted-text
role="idx"} 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) = ...
# 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)
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)
Export marker
If a declared symbol is marked with an asterisk
{.interpreted-text
role="idx"} it is exported from the current module:
proc exportedEcho*(s: string) = echo s
proc `*`*(a: string; b: int): string =
result = newStringOfCap(a.len * b)
for i in 1..b: result.add a
var exportedVar*: int
const exportedConst* = 78
type
ExportedType* = object
exportedField*: int
Method call syntax
For object-oriented programming, 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 procedures:
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:
p[T](x)
cannot be written as x.p[T]
because x.p[T]
is always
parsed as (x.p)[T]
.
See also: Limitations of the method call syntax.
The [: ]
notation has been designed to mitigate this issue: x.p[:T]
is rewritten by the parser to p[T](x)
, x.p[:T](y)
is rewritten to
p[T](x, y)
. Note that [: ]
has no AST representation, the rewrite is
performed directly in the parsing step.
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, a special setter syntax is needed:
# 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
A proc defined as f=
(with the trailing =
) is called a
setter
. A setter can be called
explicitly via the common backticks notation:
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.
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"
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 can\'t have complex expressions as
arguments. For example: (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.
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). The closure environment may be allocated on the heap or on the stack if the compiler determines that this would be safe.
Creating closures in loops
Since closures capture local variables by reference it is often not wanted behavior inside loop bodies. See closureScope and capture for details on how to change this behavior.
Anonymous Procs
Unnamed procedures can be used as lambda expressions to pass into other procedures:
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 the
=>
macro which enables a more succinct syntax for anonymous procedures
resembling lambdas as they are in languages like JavaScript, C#, etc.
Func
The func
keyword introduces a shortcut for a
noSideEffect
proc.
Is short for:
Routines
A routine is a symbol of kind: proc
, func
, method
, iterator
,
macro
, template
, converter
.
Type bound operators
A type bound operator is a proc
or func
whose name starts with =
but isn\'t an operator (i.e. containing only symbols, such as ==
).
These are unrelated to setters (see
properties), which instead end in
=
. A type bound operator declared for a type applies to the type
regardless of whether the operator is in scope (including if it is
private).
# foo.nim:
var witness* = 0
type Foo[T] = object
proc initFoo*(T: typedesc): Foo[T] = discard
proc `=destroy`[T](x: var Foo[T]) = witness.inc # type bound operator
# main.nim:
import foo
block:
var a = initFoo(int)
doAssert witness == 0
doAssert witness == 1
block:
var a = initFoo(int)
doAssert witness == 1
`=destroy`(a) # can be called explicitly, even without being in scope
doAssert witness == 2
# will still be called upon exiting scope
doAssert witness == 3
Type bound operators are: =destroy
, =copy
, =sink
, =trace
,
=deepcopy
.
For more details on some of those procs, see Lifetime-tracking hooks.
Nonoverloadable builtins
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
{.interpreted-text
role="idx"} 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
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 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 equivalent 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:
Note: var
parameters are never necessary for efficient parameter
passing. Since non-var parameters cannot be modified the compiler is
always free to pass arguments by reference if it considers it can speed
up execution.
Var return type
A proc, converter, or iterator may return a var
type which means that
the returned value is an l-value and can be modified by the caller:
It is a static error if the implicitly introduced pointer could be used to access a location beyond its lifetime:
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.
Future directions
Later versions of Nim can be more precise about the borrowing rule with a syntax like:
Here var T from container
explicitly exposes that the location is
derived from the second parameter (called \'container\' in this case).
The syntax var T from p
specifies a type varTy[T, 2]
which is
incompatible with varTy[T, 1]
.
NRVO
Note: This section describes the current implementation. This part of the language specification will be changed. See https://github.com/nim-lang/RFCs/issues/230 for more information.
The return value is represented inside the body of a routine as the
special result
variable. This allows for
a mechanism much like C++\'s \"named return value optimization\"
(NRVO
). NRVO means that the stores to
result
inside p
directly affect the destination dest
in
let/var dest = p(args)
(definition of dest
) and also in
dest = p(args)
(assignment to dest
). This is achieved by rewriting
dest = p(args)
to p'(args, dest)
where p'
is a variation of p
that returns void
and receives a hidden mutable parameter representing
result
.
Informally:
proc p(): BigT = ...
var x = p()
x = p()
# is roughly turned into:
proc p(result: var BigT) = ...
var x; p(x)
p(x)
Let T
\'s be p
\'s return type. NRVO applies for T
if
sizeof(T) >= N
(where N
is implementation dependent), in other
words, it applies for \"big\" structures.
If p
can raise an exception, NRVO applies regardless. This can produce
observable differences in behavior:
type
BigT = array[16, int]
proc p(raiseAt: int): BigT =
for i in 0..high(result):
if i == raiseAt: raise newException(ValueError, "interception")
result[i] = i
proc main =
var x: BigT
try:
x = p(8)
except ValueError:
doAssert x == [0, 1, 2, 3, 4, 5, 6, 7, 0, 0, 0, 0, 0, 0, 0, 0]
main()
However, the current implementation produces a warning in these cases. There are different ways to deal with this warning:
- Disable the warning via
{.push warning[ObservableStores]: off.}
...{.pop.}
. Then one may need to ensure thatp
only raises before any stores toresult
happen. - One can use a temporary helper variable, for example instead of
x = p(8)
uselet tmp = p(8); x = tmp
.
Overloading of the subscript operator
The []
subscript operator for arrays/openarrays/sequences can be
overloaded.