I've been using Nim for about 1-2 years now, and I believe the language is undervalued. It's not perfect, of course, but it's pleasant to write and read. My personal website uses Nim.
After reading a recent article on Nim ("Why Nim") and the associated HN comments, it's clear that comments and some information about Nim are misleading and outdated. Since Nim 2, a tracing Garbage Collector is not the default nor the recommended memory management option. Instead, the default memory management model is ORC/ARC, which supports C++-esque RAII with destructors, moves, and copies. When you use ref types, your object instances are reference-counted, similar to a shared_ptr in C++, but it does not include atomic counters by default (use the switch --mm:atomicArc , which will likely be the default in Nim 3*).
In fact, you could use Nim as a production-ready alternative to the upcoming Carbon language. Nim has fantastic interoperability with C++, supporting templates, constructors, destructors, overloaded operators, etc. However, it does not compile to readable C or C++ code, which is unlike Carbon's goals.
In this article, I review the good and bad parts of Nim 2. I'll write a tiny key/value file format that can load a user-declared object to demonstrate how some of Nim's features compose in powerful ways. Hopefully, this code example will give a good feel for the language. If you prefer to start with code, feel free to jump to the example first.
I'm not going to discuss subjective dismissals of the language, such as whitespace or case insensitivity, which IMO are not reasons to dismiss a language.
* The Nim team is currently working on Nim 3 (called Nimony), a new iteration of the language. The largest design change is NIF, an intermediate human-readable format (similar to Lisp). NIF enables incremental compilation, a better macro system (simpler to implement), and better tooling. Here is a link to a document describing the design and associated blog post.
A common question I see online is: What sets Nim apart? In other words, why should you use Nim over any other language it competes with, such as C++, Go, Rust, JavaScript, or Python? In my opinion, there isn't just one unique "ground-breaking" feature or quality that sets Nim apart from the pack: it's the language as a whole. Overall, Nim is one of the most concise, flexible, and performant languages publicly available.
Nim is a systems programming language that feels much like a high-level scripting language like Python, as it generally requires less code to do actual work (minimal boilerplate; here's a chatroom in 70 LOC), i.e. it is concise. Nim is flexible as it has some of the best meta-programming capabilities and can be compiled to JavaScript (for a web frontend) or to a native executable (via C, C++, or Objective-C); it has arbitrary compile-time execution, that is: any code written in Nim can execute at compile-time. Nim can produce code that is similar in performance to other systems programming languages (C, C++, Rust, Odin, Zig). If you need to squeeze out extra performance, you can write lower-level style code, use SIMD intrinsics (e.g. using nimsimd) and/or generate code, e.g. here's how to generate CUDA code at compile-time with an emit pragma.
Here is an overview of some of Nim 2's features that make the language a joy to write in and hopefully will allow you to gain an idea of what Nim offers. This list is grouped by category from most to least commonly used:
Here's the code for the full example in the Nim Playground or as a gist.
My favourite combination of features in Nim has to be fieldPairs and procedure overloading. This combination of features enables an easy way to serialize and de-serialize types to/from various sources (databases, files, a network, etc.) without requiring external code generation scripts.
To demonstrate this combination of features, I'll write a simple key/value file format. For example, say we wanted to load configuration from a text file. The schema of the file would be defined by a user-defined object, such as:
type Config = object name: string lr: float betas: seq[float]
Here is an associated file for the Config object, if using = as a separator for keys and values:
name=my experiment lr=0.001 betas=[0.99, 0.999]
To implement this, we will need to define a load[T] function that accepts a file path and returns a T . The load function will iterate over the fields of the type T and call an overloaded proc called parseValue to parse each value string. I will provide parseValue for some primitive types. Let's see how to implement load :
import std/[parseutils, strformat, strutils, tables, sugar, enumerate] type ParseError* = object of CatchableError proc parseValue*(x: var int, value: string): bool = parseInt(value, x) > 0 proc parseValue*(x: var float, value: string): bool = parseFloat(value, x) > 0 proc parseValue*(x: var string, value: string): bool = x = value return true proc load*[T: object](fp: string, sep: string = "="): T = let content = readFile(fp) kvs = collect: for i, line in enumerate(content.splitLines): if line.len == 0: continue let kv = line.split(sep) if kv.len != 2: raise newException(ParseError, &"line: {i}, expected a key and value pair seperated by {sep}" & &"got instead {kv.len} seperations, line content: {line}" ) {kv[0]: kv[1]} for name, value in result.fieldPairs: if name in kvs: if not parseValue(value, kvs[name]): raise newException(ParseError, "could not parse field: '" & $name & "' with specified value: " & kvs[name] & " (expected type is: " & $typeof(value) & ")" ) # NOTE: the below line wont work due to how fieldPairs works (`name` & `value` are mangled) # raise newException(CatchableError, &"could not parse '{name}' ...")
If a user needs need custom parsing support for their type, they can provide more overloaded versions of parseValue . I'll provide an overload for seq[T] as the Config type uses a seq[float] .
proc parseValue*[T](xs: var seq[T], value: string): bool = if not value.startsWith("[") and not value.endsWith("]"): raise newException(ParseError, "expected seq to start and end with '[' and ']'") for value in value[1..^2].split(","): var tmp: T if not parseValue(tmp, value.strip(trailing=false)): raise newException(ParseError, &"could not parse {value} as type {$T}") xs.add tmp return true
Now we can load a Config object at runtime:
let config = load[Config]("config.txt") echo config
and at compile-time, by changing let to const - nice!
const config = load[Config]("config.txt") static: # a static block executes each statement at compile time echo "Using configuration:" echo config
I'll leave handling nested types as an exercise to the reader (hint: you'll likely have to change the format a bit).
Using Nim is not all sunshine and rainbows. Nim's weaknesses, in my opinion, are with respect to tooling and some minor things with the compiler and language. Also, in my opinion, to get the most out of Nim, you should understand and know clang/GCC's compiler switches (or the C compiler of your choice, such as MSVC).
Here's a list of my nit-picks with Nim, they're nits as they will not block you from developing productively with the language. Feel free to correct me in the comments about any of the following points:
Tooling : The LSP could be faster, and it sometimes crashes due to syntax errors or produces zombie processes. Debugging Nim is not fun. Names are mangled twice, once due to the Nim compiler and again due to the C or C++ compiler. Pressing tab to expand an identifier in GDB or LLDB's TUI/CLI is not ideal. Assertions ( assert / doAssert ), libraries to help with fuzzing, and unit tests help prevent having to debug your code in the first place. You can't debug NimScript.
: Compiler and Language Design Compile times are reasonably fast, but they could be better. I've never waited >5s for a build, usually <1s, but I've been dealing with <50K LOC projects. Compile times are slower than they could be because of the following reasons: no incremental build support, and no LLVM backend (or custom IR) - that is, compiling to C first has some overhead. nlvm is an LLVM backend for Nim, but I haven't used it. Nim3 will have incremental builds and maybe an official LLVM backend. Some language features can be confusing to newcomers. For example, iterators are by default inline iterators (due to performance benefits), but inline iterators cannot be passed around to procedures. Instead, {.closure.} iterators are required for this use-case, or alternatively, you must use a template or macro with inline iterators. Coming from a C++ mindset for iterators, it can be confusing. You cannot forward arguments ( varargs ) to a function with some prefixed arguments easily, e.g. the equivalent code for this Python snippet requires you to write a macro: foo(1, "a", *args, **kwargs) . An ... operator (similar to C++) could solve this, to expand the arguments inline, i.e. foo(1, "a", args...) . Writing macros can be difficult and requires compile-time execution. In some cases, having access to the macro's API at run-time would be better, e.g. to get around the restrictions of NimScript (debugging, FFI). The current solution is to import the compiler's parser API, which is a similar, but different API to a Nim macro. Nim3's NIF will solve this problem.
Standard Library: WASM is not supported in the standard library. You can compile to WASM with clang (with or without emscripten), but you'll need to write your own bindings as the {.importjs.} pragma is not supported when targeting WASM. You can define your own {.importjs.} pragma equivalent, or do what emscripten in C/C++ land does ( EM_JS , etc.), but it would be nice if this were supported out of the box. The standard library deserves a potential redesign, due to some new language features introduced since its original conception.
Overall, Nim is a great systems programming language. It's opened my eyes to what a programming language can be. You don't need to write a lot of code in Nim to do something useful, and it's pretty easy to write code that can generalize. Nim has a small community, but some libraries are really high quality. For example, if you want to write a CLI tool in the language, then check out cligen for argument parsing. cligen is similar to click in Python-land.
Here are some other third-party libraries I recommend checking out if you use the language, in no particular order: