Nim at a Glance — A Language a Day, Advent Calendar 2019 Day 15/24

About this ‘A Language a Day’ Advent Calendar 2019

Welcome to Day 15 of this year’s A Language a Day Advent Calendar. Today’s topic is introduction to the Nim programming language.

Facts about the language

Some facts about the Nim programming language:

  • A statically-typed language
  • Compiles to C, C++, JavaScript, and Objective-C
  • Semi-case-insensitive identifiers (see below)
  • Uses Python-like code indentation
  • Appeared in 2008
  • Website: nim-lang.org

Installing and running Nim

To install Nim on your operating system, consult the Downloads page and find the correct instructions. On a Mac, you can install it with brew:

$ brew install nim

To run a program, you need to compile it first (note the c command):

$ nim c helloworld.nim 

After this, you obtain a binary executable file that you can run:

$ ./helloworld

You can also do both actions in a single go (but you will also see a number of messages printed during the compile stage).

$ nim c -r helloworld.nim 

Hello, World!

Here is the Hello, World! program in Nim:

echo "Hello, World!"

If you prefer, you can add parentheses:

echo("Hello, World!")

Variables

Variables are created with the var keyword. In the next example, we are using a variable to keep a string:

var name = "John"
echo "Hello, ", name, "!"

In this program, the type of the variable can be deduced by the compiler. In a general case, you specify it explicitly:

var name: string = "John"

You can also create constants with the const keyword; they must be computable at compile time. You can create a variable with the let keyword, in which case you can only assign a value once.

const x = 42
echo x
# x = 42

let y = readLine(stdin)
echo y
# y = "another value"

Identifier names

An unusual feature of Nim is how it treats the names. To compare two identifiers, Nim checks if they start with the same letter, including the case, but the rest of the name is case-insensitive. Underscores are ignored.

The following program uses different variable names but they all are recognised as the same variable:

var UserName = "Alla"
echo User_Name
echo Username
echo USERNAME
# echo username

The last example is an error, as the first-letter check is case-sensitive.

Procedures

Nim uses the word procedure to describe a function, regardless if it returns a value or not. To define a procedure, use the proc keyword.

proc greet(name: string) =
  echo "Hello, ", name, "!"

greet("John")

If you return a value, you can do it explicitly with the return keyword, or simply by assigning the value to the pre-defined result variable:

proc sqr(n: int): int =
  result = n * n
  echo "The result will be ", result
  # return result

echo sqr 5

If you don’t want to use the result of the procedure, you have to explicitly discard it, otherwise it will be a compile error.

discard sqr 5

Nim allows overloading procedure names to obtain multiple dispatch:

proc info(s: string) =
  echo "This is a string"

proc info(i: int) =
  echo "This is an integer"

info 42   # This is an integer
info "42" # This is a string

Factorial

Computing a factorial is a quite straightforward routine:

proc factorial(n: int): int =
  if n < 2: 1
  else: n * factorial(n - 1)

echo factorial 1 # 1
echo factorial 5 # 120

Notice here that you do not need to type the return keyword. Let us look at another version of a function that uses a range and the result variable.

proc factorial(n: int): int = 
  result = 1
  for i in 1..n: result *= i

Mutable function arguments

As a side note, you could think of implementing the factorial function using the while loop with a decrement of the argument:

proc factorial(n: var int): int =
  result = 1
  while n > 1:
    result *= n
    dec n

var x = 5
echo factorial(x) # 120
echo x # 1

Here, the parameter n of the function is mutable, but this means that the variable that you pass to the function will be changed after returning from it. Also, you will not be able to call a function with a literal value, for example, factorial(5).

Object-oriented programming

From the object-oriented programming world, we first want to know how to create an object with data elements in it, how to create methods, and how to do hierarchy.

Here is a class denoting a Person with the two fields: name and age. Note the syntax with the typeobject keywords.

type
  Person = object
    name: string
    age: int

To create an object, call its constructor and pass the named parameters to it:

var p = Person(name: "John", age: 40)

Methods are emulated via regular procedures which take an object as the first parameter. If the method wants to change the object, mark the parameter as var.

proc info(o: Person): string =
  o.name & " is " & $o.age & " years old"

The two new elements here are the & (string concatenation) and $ (stringification of an object, an integer variable in our case).

There are two equivalent ways of calling such a method:

echo info(p)
echo p.info

A Polymorphic example

Nim allows single inheritance, and you do it with the of keyword. Note in the next example, that you have to make the children object references (via ref), and the base class has to be inherited from RootObj (otherwise you get an error saying that final classes are not inheritable).

type
  Animal = object of RootObj

  Cat = ref object of Animal
  Dog = ref object of Animal

  zoo_t = array[0..3, ref Animal]

proc info(x: ref Animal): string =
  if x of Cat:   return "Cat"
  elif x of Dog: return "Dog"

var zoo: zoo_t = [Cat(), Dog(), Cat(), Dog()]

for _, x in zoo:
  echo info(x)

So, Animal is a type object derived from the top-level RootObj. Both Cat and Dog are the refs derived from Animal. In the type section, we also define the type of the array for storing our future objects.

It is not directly possible to define two functions such as info(x: Cat) and info(x: Dog) and later call info(zoo[i]). Nim will treat this call as a call to info(x: ref Animal). That is why the dispatch is done inside a single function. The of operator returns true if it sees that the currently passed object is one of the tested type.

Concurrency

Nim supports threads and allows implementing parallel algorithms. Let us look at threads on the Sleep sort example.

Sleep Sort

Here is the full code of the Sleep sort implementation that uses threads. Simply add spawn to call a procedure as a coroutine in a thread (whatever it is internally).

import os, threadpool

proc sortMe(n: int) =
  sleep n * 100
  echo n

var data = [10, 4, 2, 6, 2, 7, 1, 3]
for _, x in data:
  spawn sortMe x

sync()

To join the threads, call sync() at the end of the program.

Note that you have to compile this program with the following command-line option:

$ nim c -r --threads:on sleepsort.nim 

It is interesting to check what Nim offers for parallel algorithms (which is experimental at the moment).

Get more

That’s it for today, but Nim is more than what we saw so far. Refer to the following resources to learn more about it:

You can find the source code with today’s examples on GitHub as usual.

Next: Day 16. V

Leave a Reply