About this ‘A Language a Day’ Advent Calendar
This article became a part of my book ‘A Language a Day’, which you can get in both electronic and paper format. Languages covered in the book: C++, Clojure, Crystal, D, Dart, Elixir, Factor, Go, Hack, Hy, Io, Julia, Kotlin, Lua, Mercury, Nim, OCaml, Raku, Rust, Scala, and TypeScript.
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 = 42let 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 type
… object
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 ref
s 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:
- Official documentation with tutorials, style guides, and library references
- Let’s Explore Object Oriented Programming with Nim
You can find the source code with today’s examples on GitHub as usual.
Next: Day 16. V