Things I Like About Nim

Method Call Syntax

In Nim, a.f(b) is really just syntactic sugar for f(a, b). Note: they look like methods but aren’t attached to the type/object at all. Which function to call will be figured out at compile time, just like normal function calls.

You can use this to implement pseudo extension methods on standard types like strings:

import
  nim_utils/files,
  os,
  strformat

proc makeBackup(filename: string) =
  let backupPath = fmt"{filename}.bak"
  case filename.fileType
  of ftFile, ftSymlink:
    copyFile(filename, backupPath)
  of ftDir:
    copyDir(filename, backupPath)
  else:
    raise newException(IOError, fmt"Cannot make backup of {filename}")


for f in @["fstab", "systemd"]:
    fmt"/etc/{f}".makeBackup() # Same as makeBackup(fmt"/etc/{f}")

You can also use this to chain calls together nicely:

proc execOutput*(x: openArray[string]): string # Defintion removed for brevity

proc execOutput*(command: string): string =
  command
    .split(" ")
    .filterIt(it != "")
    .execOutput() # Note this is the other execOutput, not a recursive call

defer

Nim has defer, and it is exactly like in Golang. It is called at the end of the function executing, regardless of the function returning normally or raising an exception. Very useful for making sure things get cleaned up.

proc readFile(filename: string): string =
  f = open(filename)
  defer: f.close()
  return readAll(f)

result

There’s an implicit variable in every Nim function (Nim calls them procs, but I’m gonna use function) called result. This is the return value of the function. Why do I like this pattern over just using return statements? Because it makes it easier to perform actions on the return value after you have decided what it is.

For example, while debugging you might want to print the result out.

proc foo(x: int): bool =
  if x < 3:
    result = true
  else:
    result = false

  echo fmt"{result=}" # Format string "{var=}" expands to "var={var}" like in python

Or you might want to use the result to update some metrics:

metrics
 .healthCheckStatus
 .labels(target.host, "docker", if result: "success" else: "failure")
 .inc()

You can even combine this with defer to handle explicit returns:

import
    strformat,
    unittest

proc foo(x: string): int =
    defer:
        echo fmt"{x=} {result=}"
    case x
    of "bar":
        return 3
    else:
        return -1

check foo("bar") == 3
check foo("baz") == -1

will print out

x=bar result=3
x=baz result=-1

Implicit returns

You don’t have to write returns in functions because functions will return the value of their last statement. This can lead to cleaner looking functions, although I mainly use result to make it more obvious what is happening.

proc x(a,b: int): int =
  echo "Multiplying"
  a * b

assert 2.x(2) == 4

This also works with most statements that return a value. (Annoyingly, case statements are not one of these)

proc abs(x: int): int =
  if x < 0:
    -x
  else:
    x

assert abs(1) == 1
assert abs(-4) == 4

Named and default parameters

Most languages include this nowadays, and I miss it when I can’t do it!

import std/options

proc foo(bar: int, baz: Option[int] = none(int)): int =
  if baz.isSome():
    echo "Hey we got a baz!"
    bar + baz.get()
  else:
    bar

assert foo(3) == foo(bar=1, baz=some(2))

Function Calls Without Parentheses

This can be a useful feature. Sometimes it will make your code look cleaner, other times it will harder to tell what functions are called on what args.

proc foo(n: int): int = n + 2

assert foo(2) == foo 2

Or we could have called our earlier makeBackup proc like:

makeBackup "/etc/fstab"

making Nim almost resemble a shell script, but with actual types and higher level programming tools.

proc runScriptFromUrl*(url: string) =
  let (f,path) = createTempFile("","")
  let script = execOutput fmt"curl -fsSL {url}"
  f.write(script)
  f.close()
  execCmdOrThrow fmt"chmod +x {path}"
  execCmdOrThrow fmt"sh -c {path}"

Variant types

Also called Algebraic Data Types (ADTs), Variant types are a very useful way of modeling data. They are often found in functional programming languages, and the gist is simple: different “kinds” of the same type can hold different information. For example, here’s how I would model the YAML document format in OCaml:

type ynode =
  YNil
  | YString of string
  | YMap of (string, ynode) Map.t
  | YList of ynode list

and here’s how I modeled it in Nim: source

type
  YNodeKind* = enum
    ynString, ynList, ynMap, ynNil
  YNode* = object
    case kind*: YNodeKind
    of ynNil:
      discard
    of ynString:
      strVal*: string
    of ynList:
      listVal*: seq[YNode]
    of ynMap:
      mapVal*: TableRef[string, YNode]

It’s definitely clunkier to use than in most other languages, but at least Nim supports the concept!

Templates

Nim has a powerful Macro system that works on the raw AST of the language. Slightly less powerful, but infinitely easier to understand are templates. Macros and templates are run before compilation and are used to abstract out functions on pieces of code, rather than data.

What does this mean practically? Reusable snippets of code that go beyond what functions can do

For example, this function has a lot of code with the same shape over and over:

proc getPackageManger*(): Option[PackageManager] =
  case idLike():
    of "debian":
      if exeExists(pmApt.exeName):
        return some(pmApt)
    of "arch":
      if exeExists(pmYay.exeName):
        return some(pmYay)
      if exeExists(pmPacman.exeName):
        return some(pmPacman)
  none(PackageManager)

We can abstract it into a template to make the logic more readable:

proc getPackageManager*(): Option[PackageManager] =

  template tryReturn(x: PackageManager) =
    if exeExists(x.exeName):
      return some(x)

  case idLike():
    of "debian":
      tryReturn pmApt
    of "arch":
      tryReturn pmYay
      tryReturn pmPacman
  none(PackageManager)

Another useful tool is templates that take bodies.

For example, a template that takes a block of code and runs it after cding into another directory:

template withShDir*(newDir: string, body: untyped) =
  let currDir = getCurrentDir()
  setCurrentDir newDir
  try:
    body
  finally:
    setCurrentDir currDir

# Example usage:
withShDir "/etc":
  echo "In " & getCurrentDir()
  let fstab = readFile("fstab")
  echo fstab

echo "Back in " & getCurrentDir()

This can also be used to implement the with open pattern from Python:

template withOpen*(f: var File, filename: string, body: untyped) =
  f = open(filename)
  try:
    body
  finally:
    f.close()

# Example usage:
var memInfo: File
withOpen(memInfo, "/proc/meminfo"):
  echo "Opened the file!"
  # do stuff with memInfo here

collect()

collect is a macro from the Nim standard library. It serves a similar purpose to comprehensions in python.

import std/[sets, tables]

let data = @["bird", "word"]

## seq:
let k = collect(newSeq):
  for i, d in data.pairs:
    if i mod 2 == 0: d
assert k == @["bird"]

## seq with initialSize:
let x = collect(newSeqOfCap(4)):
  for i, d in data.pairs:
    if i mod 2 == 0: d
assert x == @["bird"]

## HashSet:
let y = collect(initHashSet()):
  for d in data.items: {d}
assert y == data.toHashSet

## Table:
let z = collect(initTable(2)):
  for i, d in data.pairs: {i: d}
assert z == {0: "bird", 1: "word"}.toTable

Documentation generation is built into the compiler

The nim doc command can be used to generate documentation for Nim code. Use the --project flag to generate documentation for all reachable modules.

There’s also a macro to define examples for your documentation, and nim doc will make sure those examples pass or the build will fail.

For example, this function from yanyl

proc toInt*(n: YNode): int =
  ## Get the int value of the node
  ##
  ## Throws if `n` is not a string
  runnableExamples:
    let n = newYString("123")
    doAssert n.toInt() == 123

  expectYString n:
      result = parseInt(n.strVal)

will generate documentation that looks like this. (This documentation is hosted on Github Pages and generated by a Github Action, a pattern which I plan to copy for all future projects)

Pattern Matching

While not officially part of the language yet, pattern matching is available from the fusion package, which serves as candidates for inclusion into the standard library.

Using it requires the fusion package (which can easily be installed using the built-in nimble package manager) and enabling the experimental caseStmtMacros feature.

Let’s use it to rewrite an eariler example:

import
  std/options,
  fusion/matching

{.experimental: "caseStmtMacros".}

# proc foo(bar: int, baz: Option[int] = none(int)): int =
#   if baz.isSome():
#     echo "Hey we got a baz!"
#     bar + baz.get()
#   else:
#     bar

proc foo(bar: int, baz: Option[int] = none(int)): int =
  case baz
  of Some(@v):
    echo "Hey we got a baz!"
    result = bar + v
  of None():
    result = bar

assert foo(3) == foo(bar=1, baz=some(2))

Full documentation on pattern matching can be found here