Nim Language Highlights
In the last post I covered some things I’ve found compelling about nim
’s big picture.
Today I’ll do the same but with the language itself. If you’re still on the fence about what the big deal is, let me show you a few things I found interesting.
Let’s get scrolling.
Templates
A template
is a function which can help you reduce boilerplate and write cleaner code.
It’s kinda like a preprocessor in c
, except type-safe.
flip.nim
# define a template like a function
template safely(stuff: untyped) =
try:
stuff
except:
echo "(╯°□°)╯︵ ┻━┻"
let msg = "i am happy!"
# call the template passing a block as the parameter
safely:
echo msg[0]
echo msg[2..3]
echo msg[5..1000]
$ nim c -r flip.nim
i
am
(╯°□°)╯︵ ┻━┻
Fun fact from the manual: the !=
operator is a template which rewrites a != b
to not (a == b)
.
Neat eh?
Macros
When you ask about the killer features of nim
, many people will say macros.
Macros operate at the AST level. By evaluating the inbound code syntax, you can perform branching logic, augment it, or even rewrite it entirely. You can use macros to define DSLs or introduce amazing language features like pattern matching and interfaces.
please.nim
import macros
macro theMagicWord(statments: untyped): untyped =
result = statments
for st in statments:
for node in st:
if node.kind == nnkStrLit:
node.strVal = node.strVal & " Please."
echo "Hey!"
theMagicWord:
echo "Grab me a beer!"
$ nim c -r please.nim
Hey!
Grab me a beer! Please.
Macros are a big topic and best left for a dedicated post; don’t wait for me though:
- Steve Flenniken explains the various macro flavours
- Antonis Geralis shows a nice real-world example
- Dennis Felsing reveals
nim
’s%*
json-parsing origin story
Flexible Identifier Names
Alright. Grab your torch & pitchfork.
Identifiers, such as variable names and functions, have flexible names. It’s case insensitive (except for the first letter) and underscores are optional.
goodday.nim
proc goodDayToYou(firstName: string) =
echo "Good day to you " & firstname & "."
goodDayToYou("Joe Camel")
good_day_to_you("Medusa")
$ nim c -r goodday.nim
Good day to you Joe Camel.
Good day to you Medusa.
Did you catch the firstname
vs firstName
in the function above?
I bet you saw it and laughed at me.
You know who else saw it?
The compiler; and it knew what I meant.
Is it a big deal?
Between nimgrep
and nimsuggest
(via IDE plugins), discoverability isn’t an issue.
You can also rename functions and constants from external c
/ c++
libraries as they come in, so that’s not a problem either.
It’s just quirky and oddly satisfying when it saves your butt.
EDIT #1: Dominik Picheta points out that the practical purpose for this feature is to have freedom from the naming choices of your dependencies. You’re Team Camel but they’re Team Snake? No problem. Now you can stop avoiding eye contact with fellow devs at conferences.
Flexible Functions Calls
Let’s define a function which clips text at a ridicu…
trunk.nim
import strutils
proc truncate(value: string, length = 6, ellipsis = "..."): string =
if value.len > length:
value[0..length - 1] & ellipsis
else:
value
How do we invoke it?
Call It Like A Function
As you’d expect, you can call the function like this:
echo truncate("hello world")
# hello ...
Turns out the story doesn’t end here.
Call It Like A Method
The first parameter is a string
, so in any scope where the truncate
function is available, it is now accessible on any instances of string
.
Like extensions in swift
& c#
or mixins
in ruby
.
echo "hello world".truncate(4)
# hell...
Now you can operate in a more fluent-style where you chain functions together like a pipe.
TIL this has a name: UFCS.
Parentheses Can Be Optional
Parentheses are optional as long as there is no ambiguity.
echo "hello WORLD ".toLowerAscii.truncate(4).strip
# hell...
echo "hello WORLD ".toLowerAscii.truncate.strip
# hello ...
echo " hello WORLD ".toLowerAscii.truncate 4.strip
# Error: type mismatch: got <int literal(4)>
Call it With Named Parameters
You can call parameters by name as well like our obj-c
friends.
echo truncate(
value = "hello there how are you?",
length = 4,
ellipsis = "(lol)",
)
# hell(lol)
echo truncate("Morse Code Rulez", ellipsis = "dot dot dot")
# Morse dot dot dot
echo truncate(ellipsis = "!!!", value = "Stellar stuff.")
# Stella!!!
Flexible Function Returns
We just saw how to invoke functions, but there’s also flexibility when returning values from a function too: explicit, implicit, and result
.
Explicitly via return
is good for bailing from an early exit.
proc guess(a, b: int): string =
let x = 1.0
if a == 0: return "nope"
if b == 0: return "uh oh"
$(a / b + x)
Also, implicitly, the last expression evaluated is assigned the return value.
Another way is to assign the reserved result
variable:
proc score(a, b, c: bool, marioKartRules = true): int =
if a: result.inc
if b: result.inc
if c: result.inc
if not marioKartRules:
return
if result < 2:
result.inc 10
A few things to notice here:
result
wasn’t definedresult
wasn’t initialized- We
return
ed from a branch without a value
Again with the pitchforks? Let me explain.
- All procedures are given the
result
variable for “free” result
is set to the “default” value of appropriate return type – e.g.int
=0
,string
=""
(as of0.19
), refs arenull
- when you exit a function, the value of
result
at time of thereturn
will be its default value
Doc Tests
Doc tests allow you to tie runnable examples of your code alongside your documentation. The premise is that you’ll keep your docs up-to-date if you can’t generate them without passing the tests.
rust
and elixir
have this, as I’m sure other languages do.
This concept is fairly new to me, though.
wfw311_crypto.nim
proc encrypt*(clearText: string): string =
## Pass a clear text value to encrypt a password using
## the Windows 3.1.1 for Workgroups algorithm.
##
## Example:
runnableExamples:
doAssert "password".encrypt == "********"
doAssert "".encrypt == ""
if cleartext.len == 0:
";)"
else:
"*".repeat(clearText.len)
$ nim c -r wfw311_crypto.nim
(compiles ok)
$ nim doc wfw311_crypto.nim
system.nim(6) wfw311_crypto_examples1
system.nim(3790) failedAssertImpl
system.nim(3783) raiseAssert
system.nim(2830) sysFatal
Error: unhandled exception: /home/steve/.cache/nim/wfw311_crypto_d/runnableExamples/wfw311_crypto_examples1.nim(6, 12) `encrypt("") == ""` [AssertionError]
Running the doc
command will fail in the 2nd assertions of the runnableExamples
block. It was ";)"
instead of ""
.
Heads up: this only works with exported functions.
By the way, did you notice that I snuck another cleartext
vs clearText
?
No you didn’t.
Liar.
Exporting
A .nim
file is module and what happens inside a module stays inside a module.
Your functions, constants, and variables are isolated until you tell the compiler otherwise.
That is done with the *
suffix.
securitay.nim
let privateKey = "p@ssw0rd"
let publicKey* = "password"
publicKey
is available from other files (e.g. from securitay import publicKey
), but privateKey
is not.
If you’re familiar with javascript
, it’s like using export
(minus the gotchas).
The *
identifier feels cryptic at first, but it grows on you.
Now I wish I had it back in javascript
.
The same technique is true for properties on custom type
s.
user.nim
type
User* = object
username*: string
password: string
See how password
is missing a *
?
That means, outside this file, anyone who uses the user.password
will get an error.
This works like the fileprivate
modifier in swift
.
Defer
I first saw this in go
.
It allows you to queue up code to execute before the local scope finishes.
bad_dba.nim
proc backupDb() =
let db = openDb()
# this will always run as the last thing
# this function does
defer:
db.close()
# even if you do stuff like this:
db.lockTheUserTable()
raise newException("party-parrot")
Admittedly, I expected to use this feature more than I do.
Iterators
Iterators are functions that return more than once.
You use them in for
statements.
They are defined like a proc
except use the iterator
keyword.
Instead of return
ing values once, you yield
values multiple times.
abba.nim
import os
type
Member* = tuple[name: string, original: bool]
iterator aceOfBaseGroup(): Member =
yield ("linn", true)
yield ("jenny", true)
sleep 500 # simulate some work
yield ("ulf", true)
yield ("jonas", true)
yield ("clara", false)
echo "i saw the sign!"
for name, original in aceOfBaseGroup():
if not original:
break
echo name
$ nim c -r abba.nim
linn
jenny
(...then wait a half a sec)
ulf
jonas
In this example, we’ll never see the sign in the terminal because of the break
statement.
You can use them in long running processes where you have multiple steps or need a progress bar.
They’re also great for querying nested data structures, for example for p in paragraphTags(dom)
.
staticExec and staticRead
With staticExec
you can read a string in from a command at compile time and use it at runtime.
staticRead
does the same, but reads from a file.
static.nim
const lines = staticExec("cat static.nim | wc -l")
const source = staticRead("static.nim")
echo source
echo lines & " lines!"
$ nim c -r static.nim
const lines = staticExec("cat static.nim | wc -l")
const source = staticRead("static.nim")
echo source
echo lines & " lines!"
3 lines!
note: wc -l is counting line breaks
This is nice-to-have since you won’t have to roll your own build tools for this purpose.
when isMainModule
The when
keyword can be used for conditional compilation.
For example, you can detect platforms, OSes, compiler defines/flags (like nim -d:omg
) and more.
Nothing new here.
I was a bit surprised to see a lot of people using this for writing tests within the same module.
Maybe I’ve spent too much time in javascript
land though.
lies.nim
proc add(a, b: int): int =
a + b + 1
when isMainModule:
# this will fail, but only if you run this file
doAssert add(1, 1) == 2
You can make a module contain the implementation, documentation, and tests. All as a single file. That’s kinda cool, although I’m not entirely sure how I feel about it just yet.
Asynchronous IO
Like Node.JS, nim
has an async io mode.
You can decorate your functions with the {.async.}
pragma and then await
on future-based functions inside.
Great for writing servers.
trebek.nim
import asyncdispatch
proc alex() {.async.} =
echo "this"
await sleepAsync 1000
echo "is"
await sleepAsync 1000
echo "jeopardy!"
waitFor alex()
$ nim c -r trebek.nim
this
(...dramatic pause)
is
(...wait for it)
jeopardy!
Many of the libraries have async versions such as sockets, http clients & servers, and file-system.
Pragmas
Pragmas are declarative markers which attach to things like functions, properties, types, statements and more.
If you’re familiar with decorators
in typescript
or attributes
in C#
then you know what these are.
It’s very meta.
They are enclosed within {.
and .}
and multiple pragmas will be comma-separated.
They can also take parameters by passing a :
after the name.
Here’s a few examples of some pragmas found in the nim
standard library.
{.emit.}
{.deprecated.}
{.noSideEffect.}
{.inline.}
{.raises.}
{.importc.}
And here’s an example of {.header.}
, {.importc.}
, and {.varargs.}
working together to import a function from a c
library.
proc printf(formatstr: cstring) {.header: "<stdio.h>", importc: "printf", varargs.}
There are dozens of these built-in that do everything from provide hints to the compiler, generate code, modify the linking steps, and more.
You can build your own.
The {.emit.}
Pragma
The docs say “don’t do it” and “sloppy” and “highly discouraged”.
OTOH, when someone hands you a big red button labeled “Don’t Press”, you press it.
Ladies and gentlemen, I present… the golden escape hatch:
c_in_nim_to_c.nim
proc showtime() =
let emote = "weeee!".cstring
{.emit: """
printf("Straight to hell I go, %s!\n", `emote`);
""".}
showtime()
$ nim c -r c_in_nim_to_c.nim
Straight to hell I go, weeee!!
And it gets better.
Effect System
nim
has ways to give the compiler more information about the expectations of your functions.
If you want to disallow side-effects or ensure no exceptions can be raised, you can do this through the effect system.
For example, this won’t compile:
oops.nim
# echo writes to the terminal ... this is a side-effect
proc sayHello(name: string) =
echo "hello " & name
# a func is a proc that can't have side effects
func hi = sayHello "steve"
$ nim c -r oops.nim
oops.nim(6, 6) Error: 'hi' can have side effects
And There’s Still More
I’ll stop here, but the list keeps going!
I didn’t talk about any of the impure libraries (ones that rely on 3rd party libraries) such as postgresql
, openssl
, pcre
.
I didn’t cover any of the parsers, threading, data structures, or string utils.
if
and switch
statements are expressions.
nim secret
repls…
The official tutorial and docs have more to explore when you’re ready to dig in.
I still have the training wheels on for this language, but I’m doing c
bindings now and writing lexers and parsers.
Trust me.
I’m not a lexer and parser kinda guy.
You can probably tell. :)
I’m finding nim
comfortable and approachable despite the huge feature set.
Batteries included, as they say.