Blog - 0x00


Exploring Nim

2022/12/27

Nim is a programming language and while I was browsing reddit, someone suggested it in the comments as an alternative to Python because the syntax was similar so I decided to check it out.

According to Nims website the description is:

Nim is a statically typed compiled systems programming language. It combines successful concepts from mature languages like Python, Ada and Modula.

Efficiency is one of Nims priorities.

Style-insensitive #

One of the controversial features of Nim is the style-insensitive for variables, functions and type names. This is the first time I see a programming language with style-insensitive and doesn’t feel right. This is a valid nim file that compiles and runs without errors.

1
2
3
4
5
6
7
let foobar = true

assert(fOOBAR)
assert(f_o_o_b_a_r)
assert(foo_bar)
assert(foo_ba_r)
assert(fO_OB_AR)

This feature can be disabled so you can program just like any other language with the flag --styleCheck by default this option is off, but you can set it with hint which will compile the program but output a hint message on the variables look the same or error in which the program won’t compile if you have variables that are similar but not the same.

~ $ nim --styleCheck:error c vars.nim
vars.nim(3, 8) Error: 'fOOBAR' should be: 'foobar' [let declared in vars.nim(1, 5)]

Variable types #

In nim you have different types of variables and the compiler can infer the type of the variable based on the value making it a loosely typed language.

let #

The let type of variables are inmutable that means that once defined its value can’t be changed and can’t be redefined, meaning:

1
2
3
4
5
6
7
import std/strformat

let x = 1
echo(fmt"The value of x is {x}")

x = 2
echo(fmt"The new value of x is {x}")

~ $ nim -r c letvars.nim
letvars.nim(6, 1) Error: 'x' cannot be assigned to
And if we change in line 6 to let x = 2 we get the following error
~ $ nim -r c letvars.nim
letvars.nim(6, 5) Error: redefinition of 'x'; previous declaration here: typevars.nim(3, 5)    

var #

This type of variable is mutable, meaning the value assigned to a variable can be changed later.

1
2
3
4
5
6
7
import std/strformat

var x = 1
echo(fmt"The value of x is {x}")

x = 2
echo(fmt"The new value of x is {x}")
~ $ nim -r c varvars.nim
The value of x is 1
The new value of x is 2

const #

FInally we have constants, this like it name says are constant are inmutable. The difference with the let type of variable is that const are evaluated at compile time while let expressions are evaluated at run time. So if you know and won’t change the value then const should be used.

1
2
3
4
import std/strformat

const x = 1
echo(fmt"The value of x is {x}")
~ $ nim -r c constvars.nim
The value of x is 1

Data structures #

One of basic data structures commonly used in Python are lists. Similarly NIm provides 2 data structures like lists which I’ll explain their characteristics

Sequences #

This type of data structure behave similarly like lists in Python. You can create a sequence and add or remove elements from the sequence, the size is dynamic. Though you can’t mix different types within a list like in python where you can have ints and strings.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#[ 
  Explicitly defining the type for the sequence
  and adding the number 7 to the sequence
]#
var ages: seq[int] = @[3, 4, 2]
ages.add(7)

#[
  Uses nims feature type inference
  to create sequence and deletes element
  at position 1. (Second element)
]#
var fruits = @["apple", "watermelon", "pineapple"]
fruits.delete(1)

#[
  Throws error at compile time 
  as variables defined with type `let`
  are inmutable
]#
let woods = @["birchwood", "cedarwood", "cherry"]
woods.add("mahogany")

echo(ages[3]) # `7`
echo(fruits[1]) # `pineapple`

#[
  Throws error at run-time
  `Error: unhandled exception: index 4 not in 0 .. 3 [IndexDefect]`
]#
echo(ages[4])

#[
  Just like Python you can iterate over a sequence
  using the `for i in seq` syntax
]#
for fruit in fruits:
  echo(fruit)

In languages like C arrays once they have been initialized their size can’t be easily modified, so if you define an array of 10 elements then to add another element you have to either create a new array of size 11 and include the new element or deal manually with memory, though there is a reason rust is becoming popular because it enforces memory safety which is not easy to deal with.

Arrays #

In Nim arrays behave similarly like they do in C, they have a static size and once initialized they can’t be changed in size.

One of the features of arrays in Nim is that it checks for access out of bounds at compile time unlike C which will throw an error at run time

1
2
3
4
let fruits: array[3, string] = ["apple", "watermelon", "pineapple"]

echo(fruits[0]) # `apple`
echo(fruits[4]) # `se.nim(4, 6) Error: index 4 not in 0 .. 2`

Compiling #

Nim code, like it was mentioned in the first paragraphs, can be compiled by the nim compiler into an executable, the way it does it is by turning the nim code into C code and then uses a C compiler to turn it into an executable. For example, lets say we have a file called hello.nim with the following code

1
echo("Hello, World!")

To compile it we run $ nim c hello.nim and we get an executable hello. The generated C files will be stored in $HOME/.cache/nim/hello_d/ and if we check the files it generated in that directory you should see this:

~ $ ls .cache/nim/hello_d/
hello.json
@mhello.nim.c
@mhello.nim.c.o
@m..@s..@s.choosenim@stoolchains@snim-1.6.10@slib@sstd@sprivate@sdigitsutils.nim.c
@m..@s..@s.choosenim@stoolchains@snim-1.6.10@slib@sstd@sprivate@sdigitsutils.nim.c.o
@m..@s..@s.choosenim@stoolchains@snim-1.6.10@slib@ssystem.nim.c
@m..@s..@s.choosenim@stoolchains@snim-1.6.10@slib@ssystem.nim.c.o
@m..@s..@s.choosenim@stoolchains@snim-1.6.10@slib@ssystem@sdollars.nim.c
@m..@s..@s.choosenim@stoolchains@snim-1.6.10@slib@ssystem@sdollars.nim.c.o
@m..@s..@s.choosenim@stoolchains@snim-1.6.10@slib@ssystem@sio.nim.c
@m..@s..@s.choosenim@stoolchains@snim-1.6.10@slib@ssystem@sio.nim.c.o

The hello.json file is generated and used by the nim compiler and holds the information to compile the program with a C compiler, in this case GCC here is the output (I replaced the full home path with $HOME):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
{
  "cacheVersion": "B10310425T123831",
  "outputFile": "$HOME/hello-nim/hello",
  "compile": [
    [
      "$HOME/cache/nim/hello_d/@m..@s..@s.choosenim@stoolchains@snim-1.6.10@slib@sstd@sprivate@sdigitsutils.nim.c",
      "gcc -c  -w -fmax-errors=3   -I$Hchoosenim/toolchains/nim-1.6.10/lib -I$HOME/hello-nim -o $HOME/cache/nim/hello_d/@m..@s..@s.choosenim@stoolchains@snim-1.6.10@slib@sstd@sprivate@sdigitsutils.nim.c.o $HOME/cache/nim/hello_d/@m..@s..@s.choosenim@stoolchains@snim-1.6.10@slib@sstd@sprivate@sdigitsutils.nim.c"
    ],
    [
      "$HOME/cache/nim/hello_d/@m..@s..@s.choosenim@stoolchains@snim-1.6.10@slib@ssystem@sdollars.nim.c",
      "gcc -c  -w -fmax-errors=3   -I$HOME/choosenim/toolchains/nim-1.6.10/lib -I$HOME/hello-nim -o $HOME/cache/nim/hello_d/@m..@s..@s.choosenim@stoolchains@snim-1.6.10@slib@ssystem@sdollars.nim.c.o $HOME/cache/nim/hello_d/@m..@s..@s.choosenim@stoolchains@snim-1.6.10@slib@ssystem@sdollars.nim.c"
    ],
    [
      "$HOME/cache/nim/hello_d/@m..@s..@s.choosenim@stoolchains@snim-1.6.10@slib@ssystem@sio.nim.c",
      "gcc -c  -w -fmax-errors=3   -I$HOME/choosenim/toolchains/nim-1.6.10/lib -I$HOME/hello-nim -o $HOME/cache/nim/hello_d/@m..@s..@s.choosenim@stoolchains@snim-1.6.10@slib@ssystem@sio.nim.c.o $HOME/cache/nim/hello_d/@m..@s..@s.choosenim@stoolchains@snim-1.6.10@slib@ssystem@sio.nim.c"
    ],
    [
      "$HOME/cache/nim/hello_d/@m..@s..@s.choosenim@stoolchains@snim-1.6.10@slib@ssystem.nim.c",
      "gcc -c  -w -fmax-errors=3   -I$HOME/choosenim/toolchains/nim-1.6.10/lib -I$HOME/hello-nim -o $HOME/cache/nim/hello_d/@m..@s..@s.choosenim@stoolchains@snim-1.6.10@slib@ssystem.nim.c.o $HOME/cache/nim/hello_d/@m..@s..@s.choosenim@stoolchains@snim-1.6.10@slib@ssystem.nim.c"
    ],
    [
      "$HOME/cache/nim/hello_d/@mhello.nim.c",
      "gcc -c  -w -fmax-errors=3   -I$HOME/choosenim/toolchains/nim-1.6.10/lib -I$HOME/hello-nim -o $HOME/cache/nim/hello_d/@mhello.nim.c.o $HOME/cache/nim/hello_d/@mhello.nim.c"
    ]
  ],
  "link": [
    "$HOME/cache/nim/hello_d/@m..@s..@s.choosenim@stoolchains@snim-1.6.10@slib@sstd@sprivate@sdigitsutils.nim.c.o",
    "$HOME/cache/nim/hello_d/@m..@s..@s.choosenim@stoolchains@snim-1.6.10@slib@ssystem@sdollars.nim.c.o",
    "$HOME/cache/nim/hello_d/@m..@s..@s.choosenim@stoolchains@snim-1.6.10@slib@ssystem@sio.nim.c.o",
    "$HOME/cache/nim/hello_d/@m..@s..@s.choosenim@stoolchains@snim-1.6.10@slib@ssystem.nim.c.o",
    "$HOME/cache/nim/hello_d/@mhello.nim.c.o"
  ],
  "linkcmd": "gcc   -o $HOME/hello-nim/hello $HOME/cache/nim/hello_d/@m..@s..@s.choosenim@stoolchains@snim-1.6.10@slib@sstd@sprivate@sdigitsutils.nim.c.o $HOME/cache/nim/hello_d/@m..@s..@s.choosenim@stoolchains@snim-1.6.10@slib@ssystem@sdollars.nim.c.o $HOME/cache/nim/hello_d/@m..@s..@s.choosenim@stoolchains@snim-1.6.10@slib@ssystem@sio.nim.c.o $HOME/cache/nim/hello_d/@m..@s..@s.choosenim@stoolchains@snim-1.6.10@slib@ssystem.nim.c.o $HOME/cache/nim/hello_d/@mhello.nim.c.o    -ldl",
  "extraCmds": [],
  "configFiles": [
    "$HOME/choosenim/toolchains/nim-1.6.10/config/nim.cfg",
    "$HOME/choosenim/toolchains/nim-1.6.10/config/config.nims"
  ],
  "stdinInput": false,
  "projectIsCmd": false,
  "cmdInput": "",
  "currentDir": "$HOME/hello-nim",
  "cmdline": "",
  "depfiles": [],
  "nimexe": ""
}

The file @mhello.nim.c is the one that has contains the string “Hello, World!”

By default nim compiles in “debug mode” which like it name says it turns on certain checks and when it crashes it gives you a traceback of where in the code it failed. If you turn release mode on by passing the flag -d:release to the compiler then it will turn off checks (which might speed up your program) and won’t show a traceback when it fails.

For example, lets say we have this program, it has a function that takes a sequence and given a sequence a number it will echo the given index + 1 of that sequence

1
2
3
4
5
6
7
8
9
proc printFruits(listFruits: seq[string], i: int) =
  var x = 1
  x = x + i
  echo(listFruits[x])


let fruits: seq[string] = @["apple", "watermelon", "pineapple"]

printFruits(fruits, 4)
~ $ nim c fruit.nim
26651 lines; 0.260s; 31.629MiB peakmem; proj: fruit.nim; out: fruit [SuccessX]
~ $ ./fruit
fruit.nim(9) fruit
fruit.nim(4) printFruits
~/.choosenim/toolchains/nim-1.6.10/lib/system/fatal.nim(54) sysFatal
Error: unhandled exception: index 5 not in 0 .. 2 [IndexDefect]

~ $ nim -d:release c fruit.nim
26651 lines; 0.248s; 31.645MiB peakmem; proj: fruit.nim; out: fruit [SuccessX]
~ $ ./fruit
fatal.nim(54)            sysFatal
Error: unhandled exception: index 5 not in 0 .. 2 [IndexDefect]

As you can see debug mode shows the traceback, first the fruit.nim(9) which is the first line that calls the function printFruits and then shows the line and the name of the function where it failed fruit.nim(4) printFruits. In release mode it simply states the error that it tried to access index 5 when it only goes from 0 to 2 but not where.

Final thoughts #

Nim looks like a very promising language, similar syntax to Python but compiles to C with tools like gcc, which makes it portable.

Although the language is stable (on version 1.x.x) there are changes and discussions being made for a version 2.0.

I’d keep an eye on Nim and check it out again once they release version 2.0.