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 cd
ing 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