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% off Nim in Action with code fccpicheta.


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, and when

  • Define variables, procedures, and types: var, let, proc, type, and object

  • Handle runtime errors in your code: try, except, and finally

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

int

The integer type is the type used for the whole numbers. For example, 53.

float

The float is the type used for numbers with a decimal point. For example, 2.5.

string

The string type’s used to store multiple characters. String literals are created by placing multiple characters inside double quotes: “Nim is Awesome”.

bool

The Boolean type stores one of two values, either true or false.

char

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

int

Architecture-dependent.

32-bit on 32-bit systems,

64-bit on 64-bit systems.

 

32-bit:

-2,147,483,648 to 2,147,483,647

64-bit:

-9,223,372,036,854,775,808 to 9,223,372,036,854,775,807

Generic signed two’s complement integer. Generally, you should be using this integer type in most programs.

int8

int16

int32

int64

8-bit

16-bit

32-bit

64-bit

-128 to 127

-32,768 to 32,767

-2,147,483,648 to 2,147,483,647

-9,223,372,036,854,775,808 to 9,223,372,036,854,775,807

Signed two’s complement integer. These types can be used if you want to be

explicit about the size requirements of your data.

uint

Architecture-dependent. 32-bit on 32-bit systems, 64-bit on 64-bit systems.

32-bit:

0 to 4,294,967,295

64-bit:

0 to 18,446,744,073,709,551,615

Generic unsigned integer.

uint8

uint16

uint32

uint64

8-bit

16-bit

32-bit

64-bit

0 to 255

0 to 65,535

0 to 4,294,967,295

0 to 18,446,744,073,709,551,615

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

\r, \c

Carriage return

\l

Line feed

\t

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, download the free first chapter of Nim in Action and don’t forget that you can save 37% off the book with code fccpicheta at manning.com.