📜 ⬆️ ⬇️

Nim Tutorial (Part 1)

Note from the translator
This translation was made based on a comment from the user stas3k , in which he suggested that frol translate two parts of the “Nim Tutorial”. I was interested in it and I translated them on my own, to the best of my understanding. If anyone finds mistakes (they probably are there - in the end, the eye has already completely blinked), report in a personal, I will rule.

Introduction


“Der Mensch ist doch ein Augentier - schöne Dinge wünsch ich mir.”
(Quote from the Rammstein group's Morgenstern song. An approximate translation: “But man is a big-eyed beast - I need a lot of beautiful things.”)

This is a tutorial on the Nim programming language . It is assumed that you are familiar with basic programming concepts, such as variables, types or commands, but deep knowledge is not necessary. A large number of examples of the complex nuances of the language, you can find in the official manual. All code examples in this document follow the Nim style guide.

First program


Let's start with the modified program «hello world»:

#   echo("What's your name? ") var name: string = readLine(stdin) echo("Hi, ", name, "!") 

Save this code to the greetings.nim file. Now compile and run it:
')
 nim compile --run greetings.nim 

The --run causes Nim to automatically run the file after compilation. You can pass arguments to your program via the command line by adding them after the file name:

 nim compile --run greetings.nim arg1 arg2 

Frequently used commands and keys have abbreviations, so you can write like this:

 nim c -r greetings.nim 

To compile the release version, use the following command:

 nim c -d:release greetings.nim 

By default, the Nim compiler generates many runtime checks to simplify debugging. The -d:release switch disables these checks and enables optimization.

What the program does should be fairly obvious, but I will explain the syntax: commands written without indentation are executed when the program starts. With indentation, Nim groups commands. Indents are made only by spaces, tabs are not allowed.

String literals are enclosed in double quotes. The var command declares a new variable with the name name and type string , after which it is assigned the value returned by the readLine procedure. Because the compiler knows that readLine returns a string, you can omit the type in the declaration (this is called local type inference). So this option will also work:

 var name = readLine(stdin) 

Note that this is almost the only form of type inference that is present in Nim: it is a good compromise between brevity and readability.

The “hello world” program contains some identifiers that are already known to the compiler: echo , readLine , etc. These built-in commands are declared in the system module, which is implicitly imported by any other module.

Lexical elements


Consider the lexical elements of Nim in more detail. Like other programming languages, Nim consists of literals, identifiers, keywords, comments, operators, and punctuation.

String and character literals


String literals are enclosed in double quotes; character - in single. Special characters are escaped with a backslash \ : \n means a line break, \t means a tab, and so on. There are also raw string literals:

 r"C:\program files\nim" 

In raw literals, backslash is not an escape character.

The third and last way to write string literals is long string literals. They are framed by triple quotes: """ ... """ , may contain a newline and \ are not a escape character in them. They are very useful, for example, to include HTML fragments in the code.

Comments


A comment can be anywhere outside of a string or character literal and starts with a # . Documenting comments begin with ## :

 # . var myVariable: int ##   

The documenting comments are tokens, are included in the syntax tree and, therefore, can only be in certain places in the input file! This allows for easier documentation generators.

You can also use the discard command with long string literals to create block comments:

 discard """        Nim  -   . yes("May I ask a pointless question?") """ 

Numbers


Numeric literals are written in the same way as in most other languages. To improve readability, it is allowed to beat the digits with underscores: 1_000_000 (one million). Numbers containing a period (or e , or E ) are considered floating point literals: 1.0e9 (one billion). Hexadecimal literals begin with the prefix 0x , binary ones from 0b , and octal ones from 0o . Leading zero does not make the number octal.

var command


The var command declares a new local or global variable:

 var x, y: int #  x  y,   `int` 

Using the indents after the var keyword, you can list the whole variable section:

 var x, y: int #     a, b, c: string 

Assignment command


The assignment command assigns a new value to a variable or, more generally, a storage location:

 var x = "abc" #    `x`     x = "xyz" #    `x` 

= is an assignment operator. It cannot be overloaded, rewritten or banned, but this may change in future versions of Nim. You can declare several variables with one assignment statement and they all get the same value:

 var x, y = 3 #   `x`  `y`  3 echo "x ", x #  "x 3" echo "y ", y #  "y 3" x = 42 #   `x`  42,   `y` echo "x ", x #  "x 42" echo "y ", y #  "y 3" 

Note that declaring several variables with one assignment, which calls a procedure, can give an unexpected result: the compiler will expand the assignment and eventually call the procedure several times. If the result of the procedure depends on the side effects, your variables can get different values! To avoid this, use only constant values.

Constants


Constants are characters associated with a value. The value of a constant cannot change. The compiler must be able to evaluate the expression in a constant declaration at compile time:

 const x = "abc" #  x   "abc" 

Using the indents after the const keyword, you can list the whole section of constants:

 const x = 1 # ,     y = 2 z = y + 5 #   

let command


The let command works something like var , but declares one-time assignment variables : after initialization, their value cannot be changed.

 let x = "abc" #    `x`     x = "xyz" #  :  `x` 

The difference between let and const following: let enter a variable that cannot be reassigned, and const means "forcefully compile at compile time and put the result in the data section":

 const input = readLine(stdin) # :    

 let input = readLine(stdin) #    

Flow control commands


The welcome program contains three commands that are executed sequentially. But only the most primitive programs can work this way, the more complex ones need more cycles and branching.

if command


The if command is one of the ways to organize the execution thread branch:

 let name = readLine(stdin) if name == "": echo("Poor soul, you lost your name?") elif name == "name": echo("Very funny, your name is name.") else: echo("Hi, ", name, "!") 

Elif branches can be zero or more, the else branch is not required. The elif keyword is an abbreviation for else if , so as not to make unnecessary indents. ( "" is an empty string, it does not contain characters.)

case command


Another branching method is implemented by the case command. It divides the execution thread into several branches:

 let name = readLine(stdin) case name of "": echo("Poor soul, you lost your name?") of "name": echo("Very funny, your name is name.") of "Dave", "Frank": echo("Cool name!") else: echo("Hi, ", name, "!") 

As you can see, a comma-separated list of values ​​can be used as an argument for of .

The case command can work with integers, other enumerated types and strings. (What enumerated types will be discussed later.) For integer numbers and enumerated types, you can use value ranges:

 #     : from strutils import parseInt echo("A number please: ") let n = parseInt(readLine(stdin)) case n of 0..2, 4..7: echo("The number is in the set: {0, 1, 2, 4, 5, 6, 7}") of 3, 8: echo("The number is 3 or 8") 

However, the code above will not compile. The reason is that you need to cover all the values ​​that n can take, and the code only processes 0..8 values. Since it is not too practical to list all possible integers (although this is possible due to the notation of ranges), we will correct this by telling the compiler that for all other values ​​you don’t need to do anything:

 ... case n of 0..2, 4..7: echo("The number is in the set: {0, 1, 2, 4, 5, 6, 7}") of 3, 8: echo("The number is 3 or 8") else: discard 

An empty discard command is a command to do nothing. The compiler knows that the case command with the else section covers all possible options and, thus, the error disappears. Please note that it is impossible to cover all string values: this is why in the case of strings, the else branch is required.

In general, the case command is used for ranges of types or enumerations, where it helps that the compiler checks the coverage of all possible values.

while command


The while command is a simple loop:

 echo("What's your name? ") var name = readLine(stdin) while name == "": echo("Please tell me your name: ") name = readLine(stdin) # `var` ,       

In the example, the while used to ask the user for his name until he presses ENTER (that is, he does not enter an empty string).

For command


The for command implements a loop through all the elements of the iterator. Here is an example of using the built-in countup iterator:

 echo("  : ") for i in countup(1, 10): echo($i) # -->   1 2 3 4 5 6 7 8 9 10    

The built-in operator $ converts an integer ( int ) and many other types to a string. The variable i implicitly declared as a for loop and is of type int , since countup returns this type. i passes by the values ​​1, 2, .., 10. Each value is output using echo . This code does the same thing:

 echo("  10: ") var i = 1 while i <= 10: echo($i) inc(i) #  i  1 # -->   1 2 3 4 5 6 7 8 9 10    

The countdown is implemented as simply (but not so often needed):

 echo("  10  1: ") for i in countdown(10, 1): echo($i) # -->   10 9 8 7 6 5 4 3 2 1    

Since counting with increasing is often used in programs, Nim has an iterator .. that does the same thing as countup :

 for i in 1..10: ... 

Scopes and the block command


Flow control commands have a feature that has not yet been discussed: they open up a new scope. This means that in the following example, x not available outside the loop:

 while false: var x = "hi" echo(x) #   

The while ( for ) command creates an implicit block. Identifiers are visible only inside the block in which they were declared. The block command can be used to open a new block explicitly:

 block myblock: var x = "hi" echo(x) #    

The block label ( myblock in the example) is optional.

break command


You can exit the block ahead of time with the break command. This command can interrupt a block of while , for or block commands. It exits the nearest block if the block label is not set, from which you need to exit:

 block myblock: echo("  ") while true: echo("") break #  ,    echo("    ") block myblock2: echo("  ") while true: echo("") break myblock2 #   ( ) echo("    ") 

continue command


As in many other programming languages, the continue command immediately proceeds to the next iteration:

 while true: let x = readLine(stdin) if x == "": continue echo(x) 

when command


Example:

 when system.hostOS == "windows": echo("running on Windows!") elif system.hostOS == "linux": echo("running on Linux!") elif system.hostOS == "macosx": echo("running on Mac OS X!") else: echo("unknown operating system") 

The when command is almost identical to the if command, but there are some differences:

The when command is useful for writing platform-specific code, by analogy with the #ifdef language's #ifdef construct.
Note : To comment out a large piece of code, it is often more convenient to use the when false: construct instead of comments. It can be made repeatedly invested.

Commands and indents


Now that we’ve covered the basic flow control commands, let's go back to the Nim indent rules.

In Nim there is a distinction between simple and complex commands. Simple commands, such as assignment, procedure call, or return command, cannot contain other commands. Complex commands such as if , when , for , while may contain other commands. To avoid ambiguity, complex commands are always indented, but simple ones are not:

 #      : if x: x = false #     if: if x: if y: y = false else: y = true #  ,      : if x: x = false y = false 

Parts of commands that usually result in some value are called expressions. To improve readability, they may contain indents in certain places:

 if thisIsaLongCondition() and thisIsAnotherLongCondition(1, 2, 3, 4): x = true 

In short, indents in expressions are allowed after statements, after opening parentheses and after commas.

Using parentheses and semicolons ( ; ) you can use commands where only expressions are allowed:

 #  fac(4)   : const fac4 = (var x = 1; for i in 1..4: x *= i; x) 

Procedures


To create new commands, such as echo and readLine from the examples, we need the concept of procedures. (In some languages, they are called methods or functions.) In Nim, new procedures are defined using the proc keyword:

 proc yes(question: string): bool = echo(question, " (y/n)") while true: case readLine(stdin) of "y", "Y", "yes", "Yes": return true of "n", "N", "no", "No": return false else: echo("Please be clear: yes or no") if yes("Should I delete all your important files?"): echo("I'm sorry Dave, I'm afraid I can't do that.") else: echo("I think you know what the problem is just as well as I do.") 

This example shows a procedure called yes , which asks the user a question and returns true if he answered “yes”, and false if he answered “no”. The return command causes an immediate exit from the procedure (and, accordingly, a while ). Syntax (question: string): bool means that the procedure expects to receive a parameter named question and type string and returns a value of type bool . bool is a built-in type: the only values ​​it can take are true and false . Conditions in if or while commands must be of type bool .

A bit of terminology: in the example, the question formally called a parameter, and "Should I..." is the argument that is passed in this parameter.

Result variable


In any procedure that returns a value, the result variable is implicitly declared, which is the return value. The return command with no arguments is just a shorthand for return result . The result variable is always returned when exiting the procedure, even if there was no return command.

 proc sumTillNegative(x: varargs[int]): int = for i in x: if i < 0: return result = result + i echo sumTillNegative() #  0 echo sumTillNegative(3, 4, 5) #  12 echo sumTillNegative(3, 4 , -1 , 6) #  7 

At the time the function starts, the result variable is always already declared, so trying to declare it again, for example, using var result , will cause its normal variable to be shaded with the same name. The variable result always initialized with the default value for its type. Therefore, the reference data types will have a value of nil , so if necessary they will have to be initialized manually.

Options


In the procedure body, the parameters are constants. Since they cannot be changed by default, this allows the compiler to realize the transfer of parameters in the most efficient way. If a variable variable is needed inside a procedure, it should be declared in the body of the procedure using var . Parameter name shading is possible and sometimes used:

 proc printSeq(s: seq, nprinted: int = -1) = var nprinted = if nprinted == -1: s.len else: min(nprinted, s.len) for i in 0 .. <nprinted: echo s[i] 

If the procedure needs to modify the argument to pass to the caller, you can use the var parameter:

 proc divmod(a, b: int; res, remainder: var int) = res = a div b #   remainder = a mod b #    var x, y: int echo(x) divmod(8, 5, x, y) #  x  y echo(y) 

In the example, res and remainder are var parameters. Such parameters can be modified by the procedure and the changes will be visible to the caller. Note that in the example above, instead of the var parameters, it would be better to return a tuple.

discard command


To call a procedure that returns a value and ignore the result of calling it, you must use the discard command. In Nim, you cannot simply take and drop the returned value:

 discard yes("    ?") 

The returned value can be ignored implicitly if the callable procedure or iterator was declared with the discardable discardable :

 proc p(x, y: int): int {.discardable.} = return x + y p(3, 4) #   

The discard command can also be used to create a block of comments, as described in the Comments section.

Named Arguments


It happens that the procedure has many parameters and it is difficult to remember in what order they go. This is especially true for procedures that construct complex data types. In such cases, the procedure arguments can be named to make it clearer which argument corresponds to which parameter:

 proc createWindow(x, y, width, height: int; title: string; show: bool): Window = ... var w = createWindow(show = true, title = "My Application", x = 0, y = 0, height = 600, width = 800) 

Now that we have used named arguments to call createWindow , the order of the arguments no longer matters. Named arguments can be mixed with unnamed ones, but this degrades readability:

 var w = createWindow(0, 0, title = "My Application", height = 600, width = 800, true) 

The compiler checks that each parameter takes exactly one argument.

Default values


To make createWindow easier to use, it must provide default values, that is, those values ​​that will be used as arguments, if the caller does not specify them:

 proc createWindow(x = 0, y = 0, width = 500, height = 700, title = "unknown", show = true): Window = ... var w = createWindow(title = "My Application", height = 600, width = 800) 

Now, when calling createWindow you need to specify only those values ​​that differ from the default values.

Note that for parameters with default values, type inference works, so there is no need to write, for example, title: string = "unknown" .

Overloaded procedures


Nim makes it possible to overload procedures by analogy with C ++:

 proc toString(x: int): string = ... proc toString(x: bool): string = if x: result = "true" else: result = "false" echo(toString(13)) #   toString(x: int) echo(toString(true)) #   toString(x: bool) 

(Remember that Nim's toString is usually implemented with the $ operator.) The compiler will choose the most appropriate procedure for calling toString . How exactly the algorithm for selecting overloaded procedures works will not be discussed here (this issue will soon be dealt with in the manual). However, it does not lead to unpleasant surprises and is based on a rather simple unification algorithm. Ambiguous calls result in an error message.

Operators


The Nim library makes heavy use of overloading - one of the reasons for this is that every operator like + is just an overloaded procedure. Parser allows you to use operators in infix (a + b) or prefix notation (+ a) . The infix operator always takes two arguments, and the prefix operator always takes one. Postfix operators are forbidden, since they can lead to ambiguity: a @ @ b means (a) @ (@b) or (a@) @ (b) ? Since there are no postfix operators in Nim, this expression will always mean (a) @ (@b) .

In addition to several built-in keyword statements, such as and , or and not , operators always consist of the following characters: + - * \ / < > = @ $ ~ & % ! ? ^ . | + - * \ / < > = @ $ ~ & % ! ? ^ . |

User-defined operators are allowed. Nothing prevents you from defining your own @!?+~ Operator, but readability may suffer.

The operator’s priority is determined by its first character.Details can be found in the manual.

To define an operator, enclose it in apostrophes:

 proc `$` (x: myDataType): string = ... #   $     myDataType,   $, #       

This notation can also be used to call an operator as a procedure:

 if `==`( `+`(3, 4), 7): echo("True") 

Advance announcements


Each variable, procedure, etc. must be declared before it can be used. (The reason is that it is difficult to come up with a better solution for a language that supports metaprogramming to the same extent that Nim supports it.) However, this cannot be done in mutually recursive procedures:

 #  : proc even(n: int): bool proc even(n: int): bool proc odd(n: int): bool = assert(n >= 0) # ,        if n == 0: false else: n == 1 or even(n-1) proc even(n: int): bool = assert(n >= 0) # ,        if n == 1: false else: n == 0 or odd(n-1) 

It odddepends on evenand vice versa. Thus, the evencompiler must meet before it is fully defined. The syntax for such a preliminary declaration is simple: just skip the =body of the procedure. assertadds boundary conditions and will be described later in the Modules section .

In future versions of the language, the requirements for preliminary announcements will be less stringent.

The example also shows how the body of an operator can consist of a single expression whose value is returned implicitly.

Iterators


Let's go back to the boring example with the calculation:

 echo("  : ") for i in countup(1, 10): echo($i) 

Is it possible to write a procedure countupfor use in such a loop? Let's try:

 proc countup(a, b: int): int = var res = a while res <= b: return res inc(res) 

, . , , . « » yield . proc iterator – :

 iterator countup(a, b: int): int = var res = a while res <= b: yield res inc(res) 

, :

However, you can also use an iterator closurethat has a different set of constraints. For details, see the first class iterators documentation . Iterators can have the same names and parameters as procedures: they have their own namespace. Therefore, there is a common practice for wrapping iterators into procedures with the same names that accumulate the result of an iterator and return it as a sequence, as splitfrom a module strutils.

Base types


This section describes in detail the basic built-in types and operations that are available to them.

Boolean values


The logical type in Nim is called booland consists of two predefined values trueand false. Conditions in the commands while, if, elifand whenmust have a type bool.

For type booldefined operators not, and, or, xor, <, <=, >, >=, !=and ==. Operators andand orperform abbreviated calculations. Example:

 while p != nil and p.name != "xyz": # p.name  ,  p == nil p = p.next 

Characters


The character type in Nim is called char. Its size is one byte. Thus, it cannot be a UTF-8 character, only part of it. The reason for this is efficiency: in the overwhelming majority of cases, ready-made programs will correctly process data in UTF-8, since UTF-8 was designed specifically for this. Character literals are enclosed in single quotes.

Symbols can be compared using operators ==, <, <=, >and >=. The operator $converts charto string. Characters cannot be mixed with integers; To obtain the numerical value of a symbol, use the procedure ord. Conversion from a numerical value to a symbol is performed using a procedure chr.

Strings


String values ​​in Nim are mutable , so the operation of adding a substring to a string is quite efficient. Lines in Nim end with zero at the same time and contain a length field. The length of the string can be obtained by an inline procedure len; length never takes into account the terminating zero. Access to the trailing zero does not cause an error and often leads to simplification of the code:

 if s[i] == 'a' and s[i+1] == 'b': #   ,  i < len(s)! ... 

The assignment operator for strings copies the string. You can use the operator &to concatenate strings and addto add a substring.

String comparisons are done in lexicographical order. Any comparison operators are allowed. By convention, all strings are UTF-8 strings, but this is not necessary. For example, when reading lines from binary files, they are rather a sequence of bytes. Operation s[i]means the i-th character (and not the i-th unicode character) of the string s.

String variables are initialized by a special value called nil. However, most string operations cannot work withnil(this raises an exception) for performance reasons. Instead nilof an empty value, use an empty string "". But it ""creates a string object in the heap, so here you need to look for a compromise between memory and performance.

Whole numbers


Nim has the following built-in integer types: int, int8, int16, int32, int64, uint, uint8, uint16, uint32and uint64.

The default is an integer type int. Integer literals can have a suffix denoting their belonging to one or another integer type:

 let x = 0 # x   int y = 0'i8 # y   int8 z = 0'i64 # z   int64 u = 0'u # u   uint 

, , int .

+ , - , * , div , mod , < , <= , == , != , > >= . and , or , xor not . shl , – shr . . .

, .

, . , , EOutOfRange ( ).


Nim : float , float32 float64 .

float . float 64 .

, :

 var x = 0.0 # x   float y = 0.0'f32 # y   float32 z = 0.0'f64 # z   float64 

Major operators +, -, *, /, <, <=, ==, !=, >and >=defined for floating point numbers and correspond to the IEEE standard.

Automatic type conversion in expressions with different types of floating point types is performed: smaller types are converted to larger ones. Integer types are not automatically converted to floating point types and vice versa. For such transformations, the toIntand procedures can be used toFloat.

Type conversion


Conversion between base types in Nim is done by using the type as a function:

 var x: int32 = 1.int32 #  ,   int32(1) y: int8 = int8('a') # 'a' == 97'i8 z: float = 2.5 # int(2.5)   2 sum: int = int(x) + int(y) + int(z) # sum == 100 

Internal Type Representation


, $ , echo . $ , , .

, $ . repr , . , $ repr :

 var myBool = true myCharacter = 'n' myString = "nim" myInteger = 42 myFloat = 3.14 echo($myBool, ":", repr(myBool)) # --> true:true echo($myCharacter, ":", repr(myCharacter)) # --> n:'n' echo($myString, ":", repr(myString)) # --> nim:0x10fa8c050"nim" echo($myInteger, ":", repr(myInteger)) # --> 42:42 echo($myFloat, ":", repr(myFloat)) # --> 3.1400000000000001e+00:3.1400000000000001e+00 


New types in Nim can be defined using the command type:

 type biggestInt = int64 #  , ,    biggestFloat = float64 #    , ,    

Enumerations and object types cannot be defined on the fly, only in a command type.

Transfers


A variable relating to the type of enumeration may take only a limited set of values. This set consists of ordered characters. Each character is internally mapped to an integer value. The first character corresponds to the number 0, the second to the number 1, and so on. Example:

 type Direction = enum north, east, south, west var x = south # `x`   `Direction`;   `south` echo($x) #  "south"  `stdout` 

For enumerations can be used any comparison operators.

For the avoidance of transfers symbols can be classified: Direction.south.

The operator $can convert any value of the enumeration into its name, and the procedure ordinto the corresponding integer value.

For better interaction with other programming languages, integer values ​​can be explicitly assigned to enumeration characters. However, in any case, they should be in ascending order. A character that has not been explicitly assigned a numerical value is assigned the value of the previous character plus 1.

Explicitly numbered enumerations may contain omissions:

 type MyEnum = enum a = 2, b = 4, c = 89 

Enumerated types


Transfers without breaks, integer types char, bool(and their sub-bands) - they are called enumerated types. For enumerated types, there are several special operations:
OperationComment
ord(x)returns an integer that is used to represent the value x
inc(x)increases xby 1
inc(x, n)increases xby n; n- integer
dec(x)decreases xby 1
dec(x, n)decreases xby n; n- integer
succ(x)returns the next xitem
succ(x, n)returns the nth item followingx
pred(x)returns the predecessor x
pred(x, n)returns nth th predecessorx

Operation inc, dec, succand predcan be executed with an error, throws an exception EOutOfRangeor EOverflow. (Unless, of course, the code is compiled with exceptions checks enabled.)

Ranges


This type is a range of values ​​of an integer type or enumeration (base type). Example:

 type Subrange = range[0..5] 

Subrangethis is a range intthat can contain values ​​from 0 to 5. Assigning any other values ​​to a type variable Subrangewill result in a compilation error or runtime. The assignment of a base type to one of its ranges (and vice versa) is permitted.

The module systemdefines an important type Naturalas range[0..high(int)]( highreturns the maximum allowed value). Other programming languages ​​force unsigned integers to use natural numbers. This is often wrong : you should not be forced to use unsigned arithmetic only for the reason that numbers cannot be negative. The NaturalNim language type avoids this common programming error.

The sets


set . , :. , . :

 var s: set[int64] # Error: set is too large 

: {} . . ( ):

 type CharSet = set[char] var x: CharSet x = {'a'..'z', '0'..'9'} #   ,    # 'a'  'z'    '0'  '9' 

:
OperationDescription
A + B
A * B
A - B( A B )
A == B
A <= Bsubset ratio ( Ais a subset Bor equivalent B)
A < Bstrict subset relation ( Ais a subset B)
e in Abelonging to a set ( Acontains an element e)
e notin AA does not contain an element e
contains(A, e)A contains an element e
card(A)power A(number of elements in A)
incl(A, elem)the same as A = A + {elem}
excl(A, elem)the same as A = A - {elem}

Sets are often used for procedure flags. This is a more transparent (and type-safe) solution than the definition of integer constants that need to be combined with an operation or.

Arrays


An array is a simple fixed-size container. All its elements are of the same type. As an array index, any enumerated type can be used.

An array can be created using []:

 type IntArray = array[0..5, int] #   ,   0  5 var x: IntArray x = [1, 2, 3, 4, 5, 6] for i in low(x)..high(x): echo(x[i]) 

Notation is x[i]used to gain access to the i-th element x. When accessing the elements of an array, the boundaries are always checked (either at compile time or at run time). This check can be disabled by pragmas or by calling the compiler with the key --bound_checks:off.

Arrays are value types, like other Nim types. The assignment statement copies the contents of the entire array.

The built-in procedure lenreturns the length of the array. low(a)returns the smallest possible array index a, and the high(a)largest possible index.

 type Direction = enum north, east, south, west BlinkLights = enum off, on, slowBlink, mediumBlink, fastBlink LevelSetting = array[north..west, BlinkLights] var level: LevelSetting level[north] = on level[south] = slowBlink level[east] = fastBlink echo repr(level) # --> [on, fastBlink, slowBlink, off] echo low(level) # --> north echo len(level) # --> 4 echo high(level) # --> west 

The syntax for nested arrays (plural dimension) in other languages ​​is to add additional square brackets, since usually each dimension must be of the same type as the others. In Nim, you can use different dimensions with different index types, so the nesting syntax is somewhat different. Based on the previous example, where leveldefined as an array of enumerations indexed by another enum, we can add the following lines to allow the type of beacon to be divided into levels that can be accessed through their integer index:

 type LightTower = array[1..10, LevelSetting] var tower: LightTower tower[1][north] = slowBlink tower[1][east] = mediumBlink echo len(tower) # --> 10 echo len(tower[1]) # --> 4 echo repr(tower) # --> [[slowBlink, mediumBlink, ... .... #     -   #tower[north][east] = on #tower[0][1] = on 

Note that the built-in procedure lenreturns the length of the first level array only. To even better show the nested nature LightTower, one would not have to write the previous type definition LevelSetting, but instead include it directly in the first dimension type:

 type LightTower = array[1..10, array[north..west, BlinkLights]] 

Quite often, arrays start from zero, so there is an abbreviated syntax for specifying a range from zero to the specified index minus one:

 type IntArray = array[0..5, int] # ,   0  5 QuickArray = array[6, int] # ,   0  5 var x: IntArray y: QuickArray x = [1, 2, 3, 4, 5, 6] y = x for i in low(x)..high(x): echo(x[i], y[i]) 

Sequences


Sequences are like arrays, only their length can change during execution (as with strings). Since sequences can be resized, they are always heaped and participate in garbage collection.

Sequences are always indexed int, starting with 0. Operations len, lowand highapplicable to sequences. Notation x[i]can be used to access the i-th element x.

Sequences can be constructed using an array constructor []connected to an array-to-sequence conversion operator @. Another way to allocate memory for a sequence is to call an inline procedure newSeq.

A sequence can be passed in a parameter openarray.

Example:

 var x: seq[int] #      x = @[1, 2, 3, 4, 5, 6] # @    ,    

Sequence variables are initialized by value nil. However, most sequence operations cannot work with nil(this will raise an exception) for reasons related to performance. So as an empty value, it is desirable to use an empty sequence @[], and not nil. But it @[]creates a sequence object in the heap, so you will need to look for a solution that is acceptable for your particular case.

The command forused for the sequence can work with one or two variables. If you use a single variable form, the variable will contain the value provided by the sequence. The team forruns on the results obtained from the items()module iteratorsystem . , , – . for pairs() system . Examples:

 for i in @[3, 4, 5]: echo($i) # --> 3 # --> 4 # --> 5 for i, value in @[3, 4, 5]: echo("index: ", $i, ", value:", $value) # --> index: 0, value:3 # --> index: 1, value:4 # --> index: 2, value:5 


: .
, : . . 0. len , low high . , .

 var fruits: seq[string] #    ,  #   'nil' capitals: array[3, string] #      fruits = @[] #     ,  #    'fruits' capitals = ["New York", "London", "Berlin"] #  'capitals'  #     fruits.add("Banana") #  'fruits'  #     fruits.add("Mango") proc openArraySize(oa: openArray[string]): int = oa.len assert openArraySize(fruits) == 2 #    #    assert openArraySize(capitals) == 3 #     

: , .


varargs . , . :

 proc myWriteln(f: File, a: varargs[string]) = for s in items(a): write(f, s) write(f, "\n") myWriteln(stdout, "abc", "def", "xyz") #   : myWriteln(stdout, ["abc", "def", "xyz"]) 

, varargs . :

 proc myWriteln(f: File, a: varargs[string, `$`]) = for s in items(a): write(f, s) write(f, "\n") myWriteln(stdout, 123, "abc", 4.0) #   : myWriteln(stdout, [$123, $"abc", $4.0]) 

$ , a . , $ , , .


Syntax slices look similar to range types, but are used in a different context. A slice is simply an object of type Slicethat contains two borders, aand b. The slice itself is not very useful, but other types of collections define operators that accept objects Sliceto specify ranges.

  var a = "Nim is a progamming language" b = "Slices are useless." echo a[7..12] # --> 'a prog' b[11..^2] = "useful" echo b # --> 'Slices are useful.' 

In the previous example, slices are used to modify a fragment of a string. The slice boundaries can contain any value that is supported by their type, but only the procedure using the slice object determines whether it will accept this value.

Tuples (tuples)


. () . . , .

. t.field . , t[i] i - ( i ).

 type Person = tuple[name: string, age: int] #   ,  #      var person: Person person = (name: "Peter", age: 30) #  ,   : person = ("Peter", 30) echo(person.name) # "Peter" echo(person.age) # 30 echo(person[0]) # "Peter" echo(person[1]) # 30 #         . var building: tuple[street: string, number: int] building = ("Rue del Percebe", 13) echo(building.street) #    ,   ! #person = building # --> Error: type mismatch: got (tuple[street: string, number: int]) # but expected 'Person' #   ,       . var teacher: tuple[name: string, age: int] = ("Mark", 42) person = teacher 

Although you do not need to declare a type to use a tuple, tuples created with different field names will be considered different objects, even if they have the same field types.

Tuples can be unpacked in the process of assigning variables (and only in this case!). This is useful to directly assign the values ​​of the tuple fields to individual named variables. As an example, consider a procedure splitFilefrom a module osthat simultaneously returns a directory, a name, and a path extension. To properly unpack a tuple, you need to use parentheses around the values ​​into which you unpack the tuple, otherwise you will assign the same value to each of these variables! Example:

 import os let path = "usr/local/nimc.html" (dir, name, ext) = splitFile(path) baddir, badname, badext = splitFile(path) echo dir #  usr/local echo name #  nimc echo ext #  .html #        : # `(dir: usr/local, name: nimc, ext: .html)` echo baddir echo badname echo badext 

Unpacking tuples works only in blocks varor let. The following code will not compile:

 import os var path = "usr/local/nimc.html" dir, name, ext = "" (dir, name, ext) = splitFile(path) # --> Error: '(dir, name, ext)' cannot be assigned to 

Reference types and pointers


References (the same as pointers in other programming languages) are a way to organize many-to-one relationships. This means that different links can point to the same place in memory and modify it.

Nim distinguishes between traced and untraced links. Untracked links are also called pointers. Tracked links point to objects on the heap with garbage collection, untracked - on objects for which memory was allocated manually or on objects in other places of memory. Thus, untracked links are unsafe. However, for some low-level operations (access to the "hardware") can not do without them.

Tracked links are declared by the keyword ref, and untraceable - by the keyword ptr.

Empty subscript-notation []can be used to dereference a link, that is, to get the element that the link points to. Operators .(access to a tuple / object field) and [](array / string / sequence index operator) perform implicit dereferencing for reference types:

 type Node = ref NodeObj NodeObj = object le, ri: Node data: int var n: Node new(n) n.data = 9 #    n[].data,     ! 

To allocate memory for a new monitored object, a built-in procedure is used new. To work with untraceable memory, procedures can be used alloc, deallocand realloc. Module documentation systemcontains additional information on these issues.

If the link does not indicate anything, it matters nil.

Procedural type


A procedural type is (somewhat abstract) a pointer to a procedure. Variables of a procedural type may have a value nil. Nim uses a procedural type to implement functional programming techniques.

Example:

 proc echoItem(x: int) = echo(x) proc forEach(action: proc (x: int)) = const data = [2, 3, 5, 7, 11] for d in items(data): action(d) forEach(echoItem) 

A non-obvious problem with procedural types is that procedure call conventions affect type compatibility: procedural types are compatible only when they use the same calling conventions. Various calling conventions are listed in the manual .

Modules


Nim supports program partitioning according to the modular concept. Each module is in a separate file. Modules allow information hiding and separate compilation. They can access the symbols of other modules using the command import. Only top-level characters marked with an asterisk ( *) can be exported :

 #  A var x*, y: int proc `*` *(a, b: seq[int]): seq[int] = #   : newSeq(result, len(a)) #    : for i in 0..len(a)-1: result[i] = a[i] * b[i] when isMainModule: #    ``*``  : assert(@[1, 2, 3] * @[1, 2, 3] == @[1, 4, 9]) 


The module Aexports xand *, but not y.

Top-level commands in the module are executed when the program starts. This can be used, for example, to initialize complex data structures.

Each module has a special magic constant isMainModule, which is true if the module is compiled as the main file. This is very useful for embedding inside the test module, as shown in the previous example.

Modules dependent on other modules are allowed, but very undesirable, because in this case the module cannot be reused without its dependencies.

The algorithm for compiling modules is as follows:


This is best shown by example:

 #  A type T1* = int #  A   ``T1`` import B #    B proc main() = var i = p(3) # ,  B     main() 

 #  B import A # A      !     # ,    A   . proc p*(x: A.T1): A.T1 = #  ,     T1    A result = x + 1 

Module characters can be qualified using syntax module.symbol. If the character is ambiguous, it must qualify. A symbol is ambiguous if it is defined in two (or more) different modules and both modules are imported by the third:

 #  A var x*: string 

 #  B var x*: int 

 #  C import A, B write(stdout, x) # : x  write(stdout, Ax) #  :   var x = 4 write(stdout, x) #  :  x  C 

But this rule does not apply to procedures or iterators. Overload rules apply here:

 #  A proc x*(a: int): string = $a 

 #  B proc x*(a: string): string = $a 

 #  C import A, B write(stdout, x(3)) #  :  Ax write(stdout, x("")) #  :  Bx proc x*(a: int): string = nil write(stdout, x(3)) # :  `x` ? 

Character exclusion


Usually, the command importtakes all exported characters. This can be changed by specifying exclude characters with qualifier except.

 import mymodule except y 

From team


import , . from import :

 from mymodule import x, y, z 

from , , .

 from mymodule import x, y, z x() #  x   

 from mymodule import nil mymodule.x() #   x,       x() #  x       

, , .

 from mymodule as m import nil mx() # m    mymodule 

include


include , : . :

 include fileA, fileB, fileC 


( : « Nim ( 2)» .)

Source: https://habr.com/ru/post/271197/


All Articles