Nimrod is a statically typed, imperative programming language that tries to give the programmer ultimate power without compromising runtime efficiency. This means it focuses on compile-time mechanisms in all their various forms.
It uses a syntax that is a reminiscent of both Python and Pascal, has an AST-based clean macro system that is ideal for metaprogramming. It supports soft realtime GC on thread-local heaps and uses asynchronous message passing between threads, so no "stop the world" mechanism is necessary. An unsafe shared memory heap is also provided for the increased efficiency that results from that model.
This article gives a quick overview of Nimrod's many features. After an introduction to Nimrod's syntax, I show how the language allows for common procedural, functional, OO, and metaprogramming techniques while remaining simple and efficient.
Introduction to Nimrod's Syntax
Nimrod uses a conventional infix-based syntax. Like Python or Haskell, it uses indentation rather than braces to group statements. The usual control flow statements such as
for are provided.
Slightly unusual are the many ways in which function applications can be written: There is the traditional prefix notation
f(x, y). If the call is a statement, the parentheses may be omitted:
f x, y. This is called the command notation and this version of "hello world" makes use of it:
echo "hello world!"
echo "abc" is an alias for
Another notation for invoking functions is the so-called method invocation syntax:
x.f(y, z). If there is only one argument, the
() can be left out:
x.f. So you can write
x.len instead of
len(x). This way, there is no need for special getters or read-only properties.
The language clearly distinguishes between
f() because functions are first-class citizens and can be passed around like in functional programming languages.
Finally, there are "generalized string literals" that introduce yet another piece of syntactical sugar: Instead of
f("abc"), you can write
f"abc" and then backslashed escape sequences like
\n are not interpreted. This is designed for easy embedding of mini-languages like regular expressions:
re"\w+" is much easier to write and read than
Functions are called procedures in Nimrod and are declared with the
proc keyword. Unlike in C and C++, parameters are read-only unless they are declared as
var, in which case "pass by reference" is used (pass by reference is implemented with a hidden pointer).
Similar to Haskell, operators in Nimrod are simply sugar for functions. The following example declares a procedure named
++ that can take 1, 2, or 3 arguments. The value of
y defaults to 1, and
z defaults to 0.
x and adds
z to it.
proc `++`(x: var int; y: int = 1; z: int = 0) = x = x + y + z var g = 70 # ++ can then be used like this: ++g g ++ 7 g.`++`(10, 20)
Nimrod features the concept of a routine abstraction. A routine in Nimrod can be a procedure, method, template, macro, iterator, or a converter. All routines are invoked with the same syntax; thus, you cannot tell from the invocation which kind of routine it is. Similar to Lisp, Nimrod consciously decouples the syntax from the semantics to allow for powerful metaprogramming:
template `!=`(x, y: expr): expr = not (x == y) # invocation is as if '!=' were a proc: echo 34 != 33
A template is a simple form of a macro; the example shows how the unequals operator is defined in Nimrod. For metaprogramming, the type system is weakened and very general types like
stmt (statement), or
typedesc (type descriptor) are available. Note how the template is invoked like an operator.
Functional Programming with Nimrod
As I mentioned earlier, parameters that are not
var are read-only, so Nimrod has a notion of immutability. Immutability is not deep, however: As soon as any kind of pointer is involved, the location that the pointer points to can be modified:
proc modify(n: ref Node) = n.value = 45
There are two kinds of pointers in Nimrod:
ref is a pointer that is considered by the garbage collector (traced), while a
ptr is not (untraced). In general,
ptr is used for interfacing with C/C++ or to implement weak references or simply for manual memory management. Nimrod is, after all, a systems programming language.
Similar to parameters are
let variables. A
let can be assigned only once. Of course, Nimrod also has variables, which use the
let lv = stdin.readline var vv = stdin.readline vv = "abc" # valid, reassignment allowed lv = "abc" # fails at compile time
let has been designed to emulate parameter passing semantics so that
proc square(x: int): int = x*x can be emulated with:
template square(x: int): int = # ensure 'x' is only evaluated once: let y = x y * y
Finally, there is also
const, which declares true constants. Constants can't be assigned at all, not even once. Instead, their value has to be known at compile time. Nimrod has a sophisticated compile-time evaluation engine, so the following works:
proc mostSignificantBit(n: int): int = # naive algorithm: var m = n while m != 0: m = m shr 1 # 'shr' means "shift right" result += 1 result -= 1 const msb3999 = mostSignificantBit(3999)
Procs that return a value have an implicitly declared result variable that represents the return value, so there is no need to write
result is Nimrod's way to guarantee what is called return value optimization in C++.
Most variables are initialized implicitly in Nimrod and the initial value is a binary 0. Hence,
result starts with 0, which is the natural start for counting.
shr is Nimrod's shift-right operator, a keyword has been chosen to avoid confusions as
>> has the same precedence as
>. The reason for this is that Nimrod supports user-defined operators and thus needs a simple rule of how operator precedence should be handled. The (simplified) rule is that the first character of the operator determines the precedence.
Now let's get back to functional programming. Since
procs are first-class citizens, defining
filter is straightforward:
proc filter[T](a: openarray[T]; predicate: proc (x: T): bool): seq[T] = result = @ # @ constructs the empty seq for x in a: if predicate(x): result.add(x) proc map[T, S](a: openarray[T]; fn: proc (x: T): S): seq[S] = newSeq(result, a.len) for i in 0 .. >a.len: result[i] = fn(a[i])
openarray is a special type that is only valid for parameters, it is compatible with arrays and sequences. A sequence (
seq) is a growable array.
openarray is implemented as a pointer to the first element and a length. Lists instead of arrays are, of course, possible, too, but relatively uncommon. (In this, Nimrod shows its imperative roots.)
Pattern Matching and Metaprogramming
Nimrod supports product and sum types with some twists: A sum type (also called an algebraic data type) is supported by Nimrod with a classical enum plus a so-called object variant. Let's say we want to create a library for working with mathematical expressions such as
x^2 + 5*x. It's natural to define our data types like this:
type FormulaKind = enum fkVar, ## element is a variable like 'X' fkLit, ## element is a literal like 0.1 fkAdd, ## element is an addition operation fkMul, ## element is a multiplication operation fkExp ## element is an exponentiation operation type Formula = ref object case kind: FormulaKind of fkVar: name: string of fkLit: value: float of fkAdd, fkMul, fkExp: left, right: Formula
Nimrod's enum is an old-school typesafe enum, as in Ada, without any fields. To avoid name clashes, it's common to prefix the enum values with a two-letter abbreviation. The
case part in the
object declaration introduces a checked union. So the access of
f.name will raise an exception if
f.kind != fkVar. Everything in Nimrod, including
object, is a value type, but I prefer reference semantics here for easier manipulation of formulas.