Tom Campbell

Nim - A Pretty Solid Language

Published on

Friendship Ended with Go

For the last five years, I've been using shell scripts to do most of my scripting work. Shell scripting is great because of the wide range of utilities that you have access to and the relative ease with which you can get things done.

Shell scripts do, however, have their problems. For the last while, when I've needed to write something bigger, faster or more optimised, I've used Go. For the uninitiated, it's a systems-ish programming language that was developed at Google. It compiles to native binaries and is being used a ton to write server-side applications right now. Go's main talking points are that you can use it to write really simple and readable code, and that you can write a web server in a really small number of lines.

Unfortunately, the promise of simple and readable code is a bit of a lie. As far as I've experienced, Go's syntax and standard library has been simplified and streamlined to the point where trying to do anything complicated leads to unwieldy and excessively verbose code. It's also notable that Go lacks true generics, really wants you to use a specific folder structure, complains when you don't put all your projects under one directory, and other tiresome things.

So, I'm looking for something else.

Now Nim is my Best Friend

I originally found out about Nim from some random guy on /g/ who would shill it in every thread.

It seems pretty cool. Some of its features include:

- Nim code looks a lot like Python. In fact, you can just copy something from Python and there's probably an 80% chance that it will work fine.

- The Nim Compiler transpiles your code to C/C++/Obj-C/JS, so you can take advantage of all the cool features and optimisations of GCC when you're generating a binary

- Memory management isn't autistic

- The garbage collector is sane

- Writing unsafe code is easy if you need to do that

- You can overload literally everything if you want

- Nim has the best metaprogramming support I've ever seen

Now let me show you how Nim does some basic things.

Variables and Types

var a: int    # `a` is initialised to 0
echo a

var b = "b"   # `b` is a inferred to be a string
echo b

type Person* = object    # The `*` here means that Person
                         # is exported and can be used
                         # from other files that import
                         # this one.
  name: string
  age: int

var bob = Person(name: "Bob", age: 22)

# Nim lets you use camel case, snake case, or whatever
# you want to refer to the same variable
echo "These are all the same variable:"
echo bob
echo b_o_b
echo boB
Declaring and initialising variables in Nim

Now we compile it:

nim c file.nim

0
b
These are all the same variable:
(name: "Bob", age: 22)
(name: "Bob", age: 22)
(name: "Bob", age: 22)

What's notable here is Nim's case-insensitive view of variable names. While some people don't like this, I'm a firm believer that if you have two variables whose names differ only in case, you're not very good at naming things. Nim's approach here allows a programmer to write a library using camel case names, and for somebody to use that library in their code with snake case names.

Procedures

import httpClient     # Part of the standard library

# A simple process with a single argument
proc requestStatus(url: string) =
  var c = newHttpClient()
  echo c.get(url).status

requestStatus("http://google.com")

# A more complicated example
proc concatenate[T](items: varargs[T, `$`]): string =
  # Varargs here will take as many arguments as it's given
  # `T` is a generic, and as such will take arguments of any type
  # `$` tries to convert arguments into strings.
  #   You can replace this with any procedure.

  for item in items:
    result &= item & " "
    # `&` is a built in proc that concatenates strings
    # The variable `result` will be returned automatically
    #   at the end of the function

echo concatenate("I", "have", 87, "dollars")

# Templates operate on your code's AST.
# This is an example from nim's standard library
template `!=` (a, b: untyped): untyped =
  not (a == b)

# Here's another example from the Nim tutorials
template withFile(f: untyped, filename: string, mode: FileMode, body: untyped) =
  let fn = filename
  var f: File
  if open(f, fn, mode):
    try:
      body
    finally:
      close(f)
  else:
    quit("cannot open: " & fn)

withFile(txt, "ttempl3.txt", fmWrite):
  txt.writeLine("line 1")
  txt.writeLine("line 2")
200 OK
I have 87 dollars
Procedures, functions and templates in Nim

It's also notable that procedures are first-class types in Nim, meaning you can pass them around like syphilis in France during the early 1500s.

In this example, note the simple generic syntax. Nim has by far the easiest generics of any language that I've used. Simply annotate your proc with [T], use T in your arguments, and then you can switch on types later if you really want.

One of the main differences between a template and a procedure is argument evaluation. A template's arguments are evaluated lazily, whereas a proc's arguments are evaluated when it is called.

The withFile template example shows some more advanced usage of templates. Templates are a very powerful feature, and allow you to easily implement light AST manipulation.

Macros

If you want to manipulate the AST a bit more and do super complicated stuff, macros are here.

import macros

macro generate_proc() =
  var procedure = """
proc complexMath() =
  echo 2+2
"""
  result = parseStmt(procedure)

generate_proc()
complexMath()
4
Macros in Nim

If you want do a whole lot more than this, you'll need to read the docs.

Conditionals

Conditionals in Nim are a lot like those in Python.

import random

randomize()    # Seed the RNG

var
  str: string
  r = rand(1..10)    # Roll a d10
if r <= 5:
  str = "fug :DDD"
elif r >= 6:
  str = "benis"

echo case str.len:
  of 1..5:
    "'" & str & "' is 1-5 characters long"
  of 6..10:
    "'" & str & "' is 6-10 characters long"
  else:
    "String is quite long"
'fug :DDD' is 6-10 characters long
Conditionals in Nim

It's important to note that switches in Nim must cover all possible values. You can do this with a..b ranges, using else, or by stating all possible cases when you're talking about an enum.

Loops and Iterators

import strformat

while true:
  echo "Echo from inside a pointless while loop"
  break

let numbers = @[9, 8, 7, 6]
# `let` declares a variable as immutable
# `@[]` declares a sequence, which is a dynamically allocated array

for index, value in numbers:
  echo fmt"numbers[{index}]: {value}"
  # I use `fmt` here because why not

iterator countUpTo(n: int): int =
  var i: int
  while i <= n:
    yield i
    inc i

for i in countUpTo(250):
  if i mod 50 == 0: echo $i & " is a multiple of 50"
Echo from inside a pointless while loop
numbers[0]: 9
numbers[1]: 8
numbers[2]: 7
numbers[3]: 6
0 is a multiple of 50
50 is a multiple of 50
100 is a multiple of 50
150 is a multiple of 50
200 is a multiple of 50
250 is a multiple of 50
Loops and Iterators in Nim

As you can see, while loops are as you would expect and for loops are the same as in Python. The creation of custom iterators in Nim is much better than in Python. In Nim, they don't require the declaration and instantiation of a whole class and have much simpler syntax.

A Few Miscellaneous Examples

The examples below are either not discrete language features or I didn't feel the need to give them their own category.

var sequence = @["a", "b", "c"]
echo repr(sequence)
# `repr` returns a string showing a variable's representation in memory

proc seqToStr[T](arr: openArray[T]): string =
  # `openArray` is a type that can only be used for parameters, and is used to
  #    accept both arrays and sequences of any size
  for _, v in arr    # iterate over the elements, discarding the indices
    result &= $v

echo seqToStr(sequence)
0x7f64d2814048@[0x7f64d2815058"a", 0x7f64d2815080"b", 0x7f64d28150a8"c"]

abc
Type Casting in Nim
var str = "Slice me"
echo str[0..4]
echo str[6..7]
Slice
me
Slices in Nim
writeFile("/tmp/test", "Testing")
echo readFile("/tmp/test")
var file = open("/tmp/test", fmReadWrite)
file.write("Testing")
echo file.readAll
close(file)
Testing
Files in Nim

Working with files in Nim is as wonderfully easy as in Python. You can see two different ways to read/write to files above, where the first deals with opening/closing the file for you, and the second does not.

proc printf(formatstr: cstring)
{.header: "<stdio.h>", importc: "printf", varargs.}

printf("%s", "Using a C function in Nim")
Using a C function in Nim
Pragmas in Nim

You can use pragmas to access a ton of advanced and special language features. In this example, importc allows you to access a C function from Nim. For single function, you can use the form shown to declare the printf function, and the next example shows how to create fast-and-loose bindings to C++ engine.

# This is an example from the Nim Docs

{.link: "/usr/lib/libIrrlicht.so".}

{.emit: """
using namespace irr;
using namespace core;
using namespace scene;
using namespace video;
using namespace io;
using namespace gui;
""".}

const
  irr = "<irrlicht/irrlicht.h>"

type
  IrrlichtDeviceObj {.header: irr,
                      importcpp: "IrrlichtDevice".} = object
  IrrlichtDevice = ptr IrrlichtDeviceObj

proc createDevice(): IrrlichtDevice {.
  header: irr, importcpp: "createDevice(@)".}
proc run(device: IrrlichtDevice): bool {.
  header: irr, importcpp: "#.run(@)".}
Using Irrlicht from Nim

Conclusion

In conclusion, Nim's better than python and go for my purposes. I will now proceed shill Nim in /g/ threads for at least a little while.