I was asked to put together a document on why I recommend the Nim language, so here follows at brief overview of my problems with the language, what I like, and some code examples. I may update this without notice.
Nim packages can be published using the Nimble package manager. My issues with Nimble are straightforward:
- Packages can be published un-versioned
- Dependencies can be specified with version constraints, but cannot be locked to specific commits.
- Messy code, and poor error messages. The Nimble implementation is hacky and errors seem to propagate to unrelated code with bad error handling.
I have made some effort at managing Nimble using Nix, but it's always been too fragile to work consistenly.
The language has many keywords, compiler pragmas, and a flexible grammer. This can be overwhelming when writting code and complicates code comprehension. Most of the features can be ignored without safety or perfomance costs, so it shouldn't be a problem for a disciplined developer.
Compiles to C and C++
I started using Nim because I was maintaining a parts of large C++ codebase with a C++ API, but I also recognize that C++ is a deeply flawed language. Compiling to C++ allowed me to easily wrap and use these C++ ABIs. Memory management is concern when passing pointers across the threshold, but so far it hasn't been a hassle.
Wrapping the Genode C++ API:
Genode Terminal wrapper
Portable language runtime
The language runtime is fairly easy to port to modern (post-1970s) environments. The garbage collector is freestanding and easy to interface to native page allocators. Freestanding thread-local storage emulation is also available. Bare-metal is supported, but I haven't tried it.
Robust type system
The type system makes expressing array types, type templates, and distinct types easy. The language allows values to be passed around easily regardless if they are heap or stack values, with good mutability tracking.
An example of generating fixed array types along with procedure for converting to and from string encodings:
Not object oriented
The object types are easy to understand. Object inheritance is only applicable to object members, and only one level deep. There are no object "methods", access to object memembers can be restricted to the local module or allowed globally. Procedures can be added and overriden for objects at arbitrary locations. Due to the common influence of Oberon the objects are similar to those of Go. Object variants, also known as tagged unions, are one of my favorite features, and can be used in place of C++ inheritance or Go interface types.
An example of modeling multiple CBOR item types using a common object, the "kind" field desciminates which fields are in use for an object instance:
Side effect tracking
The language distinguishes between procudures and functions. Functions are a subset of producers where side effects are restricted, no I/O or other changes to global state may be made. This is helpful for implementing functional datastructures or deterministic procedures.
Deterministic cryptographic signing, the library has no side-effects and relies on an entropy gathering callback: