![]() |
From Nim in Action by Dominik Picheta
In this article you’ll learn the basics of Nim’s syntax. Learning the syntax is an important first step, as it teaches you the specific ways to write Nim code.
|
Save 37% on Nim in Action. Just enter code fccpicheta into the discount code box at checkout at manning.com.
Nim syntax
The syntax of a programming language is a set of rules that govern the way programs are written in that language. Most languages share many similarities in terms of syntax. This is true for the C family of languages, which are also the most popular. In fact, four of the most popular programming languages take a heavy syntactic inspiration from C.
Nim aims to be highly readable, and it often uses keywords instead of punctuation. Because of this, the syntax of Nim differs significantly from the C language family; instead, much of it is inspired by Python and Pascal.
Keywords
Most programming languages have the notion of a keyword, and Nim is no exception. A keyword is a word with a special meaning associated with it, when it’s used in a specific context. Because of this, one shouldn’t use these words as identifiers in your source code.
As of version 0.12.0, Nim has 70 keywords. This may sound like a lot, but you have to remember that you won’t be using most of them. Some of them don’t have a meaning and are reserved for future versions of the language; others have minor use cases.
The most commonly-used keywords allow you to do the following:
-
Specify conditional branches:
if
,case
,of
, andwhen
-
Define variables, procedures, and types:
var
,let
,proc
,type
, andobject
-
Handle runtime errors in your code:
try
,except
, andfinally
For a full list of keywords, consult the Nim manual, available at http://nim-lang.org/docs/manual.html#lexical-analysis-identifiers-keywords.
Indentation
Many programmers indent their code to make the program’s structure more apparent. In most programming languages this isn’t a requirement and serves only as an aid to human readers of the code. In those languages, keywords and punctuation are often used to delimit code blocks. In Nim, as in Python, the indentation itself is used.
Let’s look at a simple example to demonstrate the difference. The following three code samples, written in C, Ruby, and Nim, all do the same thing. But note the different ways in which code blocks are delimited.
Listing 1. C
if (42 >= 0) {
printf("42 is greater than 0");
}
Listing 2. Ruby
if 42 >= 0
puts "42 is greater than 0"
end
Listing 3. Nim
if 42 >= 0:
echo "42 is greater than 0"
As you can see, C uses curly brackets to delimit a block of code, Ruby uses the keyword end, and Nim uses indentation. Nim also uses the colon character on the line that precedes the start of the indentation. This is required for the “if” statement and for many others. However, as you continue learning about Nim, you’ll see that the colon isn’t required for all statements that start an indented code block.
Also note the use of the semicolon in listing 1. This is required at the end of each line in some programming languages (mostly the C family of languages). It tells the compiler where a line of code ends. This means that a single statement can span multiple lines, or multiple statements can be on the same line. In C you’d achieve both like this:
printf("The output is: %d",
0);
printf("Hello"); printf("World");
In Nim, the semicolon is optional and can be used to write two statements on a single line. Spanning a single statement over multiple lines is a bit more complex—you can only split up a statement after punctuation, and the next line must be indented. Here’s an example:
echo("Output: ", ❶
5)
echo(5 + ❶
5)
echo(5 ❷
+ 5)
echo(5 +
5) ❸
❶ Both statements are correct because they’ve been split after the punctuation and the next line has been indented.
❷ This statement has been incorrectly split before the punctuation.
❸ This statement has been incorrectly indented after the split.
Because indentation is important in Nim, you need to be consistent in its style. The convention states that all Nim code should be indented by two spaces. The Nim compiler currently disallows tabs because the inevitable mixing of spaces and tabs can have detrimental effects, particularly in a whitespace-significant programming language.
Comments
Comments in code are important because they allow you to add additional meaning to pieces of code. Comments in Nim are written using the hash character (#). Anything following it is a comment until the start of a new line. A multiline comment can be created with #[ and ]#, and code can also be disabled by using when false:. Here’s an example:
# Single-line comment
#[
Multiline comment
]#
when false:
echo("Commented-out code")
Nim basics
Now that you have a basic understanding of Nim’s syntax, you have a good foundation for learning some of the semantics of Nim. Let’s take a look at some of the essentials that every Nim programmer uses daily. You’ll learn about the static types most commonly used in Nim, the details of mutable and immutable variables, and how to separate commonly-used code into standalone units by defining procedures.
Basic types
Nim is a statically typed programming language. This means that each identifier in Nim has a type associated with it at compile time. When you compile your Nim program, the compiler ensures that your code is type-safe. If it isn’t, compilation terminates and the compiler outputs an error. This contrasts with dynamically typed programming languages, such as Ruby, that only ensure your code is type-safe at runtime.
By convention, type names start with an uppercase letter. Built-in types don’t follow this convention, and it’s easy for you to distinguish between built-in types and user-defined types by checking the first letter of the name. Nim supports many built-in types, including ones for dealing with the C foreign function interface (FFI). I won’t cover all of them here.
Foreign function interface
The foreign function interface (FFI) is what allows you to use libraries written in other programming languages. Nim includes types that are native to C and C++, allowing libraries written in those languages to be used.
Most of the built-in types are defined in the system module, which is imported automatically into your source code. When referring to these types in your code, you can qualify them with the module name (for example, system.int), but doing it isn’t necessary.
Modules
Modules are imported using the import keyword.
Table 1. Basic types
Type |
Description and uses |
|
The integer type is the type used for the whole numbers. For example, 53. |
|
The |
|
The |
|
The Boolean type stores one of two values, either |
|
The character type stores a single ASCII character. Character literals are created by placing a character inside single quotes. For example: ‘A’. |
Integer
The integer type represents numerical data without a fractional component; whole numbers. The amount of data this type can store is finite, and there are multiple versions in Nim, each suited to different size requirements. The main integer type in Nim is int
. It’s the integer type you should be using most in your Nim programs.
Table 2. Integer types
Type |
Size |
Range |
Description |
|
Architecture-dependent. 32-bit on 32-bit systems, 64-bit on 64-bit systems. |
32-bit:
64-bit:
|
Generic signed two’s complement integer. Generally, you should be using this integer type in most programs. |
|
8-bit 16-bit 32-bit 64-bit |
|
Signed two’s complement integer. These types can be used if you want to be explicit about the size requirements of your data. |
|
Architecture-dependent. 32-bit on 32-bit systems, 64-bit on 64-bit systems. |
32-bit:
64-bit:
|
Generic unsigned integer. |
|
8-bit 16-bit 32-bit 64-bit |
|
Unsigned integer. These types can be used if you want to be explicit about the size requirements of your data. |
An integer literal in Nim can be represented using decimal, octal, hexadecimal, or binary notation.
Listing 4. Integer literals
let decimal = 42
let hex = 0x42
let octal = 0o42
let binary = 0b101010
Listing 4 defines four integer variables and assigns a different integer literal to each of them, using the four integer-literal formats.
You’ll note that the type isn’t specified for any of the defined variables. The Nim compiler infers the correct type based on the specified integer literal. In this case, all variables have the type int
.
The compiler determines which integer type to use by looking at the size of the integer literal. The type is int64
if the integer literal exceeds the 32-bit range; otherwise it’s int
. But what if you want to use a specific integer type for your variable? There are numerous ways you can accomplish this:
let a: int16 = 42 ❶
let b = 42'i8 ❷
❶ Explicitly declares a
to be of type int16
.
❷ Uses a type suffix to specify the type of the integer literal.
Integer size
Explicitly using a small integer type such as int8
may result in a compile-time or, in some cases, a runtime error. Take a look at the ranges in table 2 to see what size of integer can fit into which integer type. You should be careful not to attempt to assign a bigger or smaller integer than the type can hold.
Nim supports type suffixes for all integer types, both signed and unsigned. The format is ‘iX, where X is the size of the signed integer, and ‘uX, where X is the size of the unsigned integer.
Floating-point
The floating-point type represents an approximation of numerical data with a fractional component. The main floating-point type in Nim is float
, and its size depends on the platform.
Listing 5. Float literals
let a = 1'f32
let b = 1.0e19
The compiler implicitly uses the float
type for floating-point literals. You can specify the type of the literal using a type suffix. Two type suffixes for floats correspond to the available floating-point types: ‘f32 for float32
and ‘f64 for float64
. Exponents can also be specified after the number. Variable b
in the preceding listing is equal to 1×1019 (1 times 10 to the power of 19).
Boolean
The Boolean type represents one of two values: usually a true
or false
value. In Nim, the Boolean type is called bool
.
Listing 6. Boolean literals
let a = false
let b = true
The false
and true
values of a Boolean must begin with a lowercase letter.
Character
The character type represents a single character. In Nim, the character type is called char
. It can’t represent UTF-8 characters; it encodes ASCII characters. Because of this, char
is a number. A character literal in Nim is a single character enclosed in quotes. The character may also be an escape sequence introduced by a backwards slash (\). Some common character escape sequences are listed in table 3.
Listing 7. Character literals
let a = 'A'
let b = '\109'
let c = '\x79'
Unicode
The unicode module contains a Rune type that can hold any unicode character.
Table 3. Common character escape sequences
Escape sequence |
Result |
|
Carriage return |
|
Line feed |
|
Tabulator |
|
Backslash |
|
Apostrophe |
|
Quotation mark |
Newline escape sequence
The newline escape sequence \n isn’t allowed in a character literal, as it may be composed of multiple characters on some platforms. On Windows it’s \r\l (carriage return followed by line feed), whereas on Linux it’s \l (line feed). Specify the character you want explicitly, such as ‘\r’ to get a carriage return, or use a string.
String
The string type represents a sequence of characters. In Nim the string type is called string
. It’s a list of characters terminated by ‘\0’. The string type also stores its length. A string
in Nim can store UTF-8 text, but the unicode module should be used for processing it, such as when you want to change the case of UTF-8 characters in a string
.
Multiple ways can be used to define string literals, such as this:
let text = "The book title is \"Nim in Action\""
When defining string literals this way, certain characters must be “escaped” within them. For instance, the double-quote character (“) should be escaped as \” and the backward-slash character (\) as \\. String literals support the same character escape sequences that character literals support; see table 3 for a good list of the common ones. One major additional escape sequence that string literals support is \n, which produces a newline
; the actual characters produced depend on the platform. The need to escape some characters makes some things tedious to write. One example is Windows filepaths:
let filepath = "C:\\Program Files\\Nim"
Nim supports raw string literals that don’t require escape sequences. Apart from the double-quote character (“), which still needs to be escaped as ""
, any character placed in a raw string literal is stored verbatim in the string. A raw string literal is a string literal preceded by an r
:
let filepath = r"C:\Program Files\Nim"
It’s also possible to specify multiline strings using triple-quoted string literals:
let multiLine = """foo
bar
baz
"""
echo multiLine
The output for the preceding code looks like this:
foo
bar
baz
Triple-quoted string literals are enclosed between three double-quote characters, and these string literals may contain any characters, including the double-quote character, without any escape sequences. The only exception is that your string literal may not repeat the double-quote character three times. There’s no way to include three double-quote characters in a triple-quoted string literal.
The indentation added to the string literal defining the multiLine
variable causes leading whitespace to appear at the start of each line. This can be easily fixed using the unindent
procedure. It lives in the strutils
module, so you must first import it.
import strutils
let multiLine = """foo
bar
baz
"""
echo multiLine.unindent
This produces the following output:
foo
bar
baz
Defining variables and other storage
Storage in Nim is defined using three different keywords. In addition to the let
keyword you can also define storage using const
and var
.
let number = 10
By using the let
keyword, you’ll be creating what’s known as an immutable variable—a variable that can only be assigned to once. In this case, a new immutable variable named number
is created, and the identifier number is bound to the value ten. If you attempt to assign a different value to this variable, your program won’t compile, as in the following numbers.nim
example:
let number = 10
number = 4000
The preceding code produces the following output when compiled:
numbers.nim(2, 1) Error: 'number' cannot be assigned to
Nim also supports mutable variables using the keyword var
. Use these if you intend on changing the value of a variable. The previous example can be fixed by replacing the let
keyword with the var
keyword.
var number = 10
number = 4000
In both examples, the compiler infers the type of the number variable based on the value assigned to it. In this case, number
is an int
. You can specify the type explicitly by writing the type after the variable name and separating it with a colon character (:). By doing this, you can omit the assignment, which is useful when you don’t want to assign a value to the variable when defining it.
var number: int ❶
❶ This will be initialized to 0.
Immutable variables
Immutable variables must be assigned a value when they’re defined because their values can’t change. This includes both const
and let
defined storage.
A variable’s initial value will always be binary zero. This manifests in different ways, depending on the type. For example, by default, integers are 0 and strings are nil
. nil
is a special value that signifies the lack of a value for any reference type.
The type of a variable can’t change. For example, assigning a string
to an int
variable results in a compile-time error as in this typeMismatch.nim
example:
var number = 10
number = "error"
Here’s the error output:
typeMismatch.nim(2, 10) Error: type mismatch: got (string) but expected 'int'
Nim also supports constants. Because the value of a constant is also immutable, constants are like immutable variables defined using let
, but a Nim constant differs in one important way: its value must be computable at compile time.
Listing 8. Constant example
proc fillString(): string =
result = ""
echo("Generating string")
for i in 0 .. 4:
result.add($i) (1)
const count = fillString()
Note
The $ is a commonly used operator in Nim that converts its input
to a string
The fillString
procedure in listing 8 generates a new string, equal to “01234”. The constant count is assigned this string. I added the echo at the top of fillString
’s body to show that it’s executed at compile time. Try compiling the example using Aporia or in a terminal by executing nim c file.nim
. You’ll see “Generating string
” amongst the output. Running the binary never displays that message because the result of the fillString
procedure is embedded in it.
To generate the value of the constant, the fillString
procedure must be executed at compile time by the Nim compiler. Be aware that not all code can be executed at compile time. For example, if a compile-time procedure uses the FFI, you’ll find that the compiler outputs an error such as “Error: cannot
'importc'
variable
” at compile time.
The main benefit of using constants is efficiency. The compiler can compute a value for you at compile time, saving time that’d be otherwise spent at runtime in your program. The obvious downside is longer compilation time, but it could also produce a larger executable size. As with many things, you must find the right balance for your use case. Nim gives you the tools, but you must use them responsibly.
You can also specify multiple variable definitions under the same var
, let
, or const
keyword. To do this, add a new line after the keyword and indent the identifier on the next line:
var
text = "hello"
number: int = 10
isTrue = false
The identifier of a variable is its name. It can contain any characters as long as the name doesn’t begin with a number and doesn’t contain two consecutive underscores. This applies to all identifiers, including procedure and type names. Identifiers can even make use of unicode characters:
var 火 = "Fire"
let ogień = true
Procedure definitions
Procedures allow you to separate your program into different units of code. These units generally perform a single task, after being given some input data, usually in the form of one or more parameters.
In other programming languages a procedure may be known as a function, method, or subroutine. Each programming language attaches different meanings to these terms, and Nim is no exception. A procedure in Nim can be defined using the proc
keyword, followed by the procedure’s name, parameters, optional return type, =
, and the procedure body. Figure 1 shows the syntax of a Nim procedure definition.
Figure 1. The syntax of a Nim procedure definition
The procedure in figure 1 is named myProc
and it takes one parameter (name
) of type string
, and returns a value of type string
. The procedure body implicitly returns a concatenation of the string literal “Hello
” and the parameter name
.
You can call a procedure by writing the name of the procedure followed by parentheses: myProc("Dominik")
. Any parameters can be specified inside the parentheses. Calling the myProc
procedure with a “Dominik
” parameter, as in the preceding example, will cause the string “Hello Dominik
” to be returned. Whenever procedures with a return value are called, their results must be used in some way.
proc myProc(name: string): string = "Hello " & name
myProc("Dominik")
Compiling this example will result in an error: “file.nim(2, 7) Error: value of type ‘string’ has to be discarded”. This error occurs as a result of the value returned by the myProc
procedure being implicitly discarded. In most cases, ignoring the result of a procedure is a bug in your code, because the result could describe an error that occurred or give you a piece of vital information. You’ll likely want to do something with the result, such as store it in a variable or pass it to another procedure via a call. In cases where you really don’t want to do anything with the result of a procedure, you can use the discard
keyword to tell the compiler to be quiet:
proc myProc(name: string): string = "Hello " & name
discard myProc("Dominik")
The discard keyword simply lets the compiler know that you’re happy to ignore the value that the procedure returns.
That’s all for this article.
If you find yourself suddenly very interested in learning more about Nim, check out the whole book on liveBook here.