By: Hector Escobedo
Published
The landscape of programming languages is dominated by a few popular behemoths, followed by a long tail of “niche” languages. As someone with both professional and hobbyist experience in a fair number of languages and paradigms, I’ve realized that popularity is not often correlated with quality when it comes to software. McDonald’s is the world’s most popular restaurant, but you shouldn’t eat there too often; similarly, while JavaScript may be the world’s most popular programming language, writing programs with it directly can be hazardous to your overall well-being. It’s true that popularity comes with genuine advantages such as free support, third-party libraries, and job security, but it isn’t everything.
Nim is a member of the long tail of the programming language market, but it’s not too far down the ladder. To call it “niche” would be a disservice to the amount of effort and care that has gone into fitting together a number of successful programming language concepts into an easily comprehensible whole. Within the design space, it lies somewhere between Rust and Go, or Python and C. It has the feel of a happy medium between low-level and high-level control, and the syntax is flexible yet pretty. In my personal opinion, the type system is the most important part of any programming language, and Nim also does better than many others in that regard. Though I’m still in the early days of learning and getting accustomed to it, I don’t think I would mind creating some significant projects with Nim. With luck it could wind up supplementing Haskell as one of my go-to choices for general purpose programming.
Best features
Here it might be helpful to note that the paradigm I have the most experience
with is functional programming, or what some might call a “data-driven” style,
and I’m not a fan of the object-oriented paradigm. While Nim is mostly
imperative, it does have support for higher-order functions, and the sequtils
module in the standard library provides the bread-and-butter list functions you
would expect in any functional language, such as map
, filter
, and even
foldl
. Therefore my analysis of the language’s features will be oriented
toward using them in a functional style.
Type inference, generics, and overloading
Bring up type inference, and a Go programmer will exclaim, “Oh yeah, that’s the
cool thing where I can type y := 10
and not have to annotate its type as an
integer!” The C programmer will just stare blankly at you. Python developers
will start to chuckle nervously, unsure if they’re really in on the joke. Nim
has full support for locally inferring the types of variables and procedures,
sometimes using the auto
type annotation. Of course, it’s still good practice
to make the majority of your types explicit. It’s not hard, either, as the
syntax is just a single colon to separate the type from the bound symbol, e.g.
uniquecs: set[char]
for a small set of character bytes.
Generics are the other killer feature that Go forgot, and which really ought
to be included in any new modern language worth its salt. Type parameters are
not that hard, people! It only makes sense that I should be able to talk about
generic container types and have parametric polymorphic functions that can
return the left-hand side of the binary tree, no matter what data type is in
the nodes! Nim has this, and even lets you specify certain type classes for
basic types, so your function can parametrically accept bool
or enum
, but
not other types.
Overloading, also known as ad-hoc polymorphism, is naturally related to these other two features, though my opinion of it is more mixed. At first it just felt wrong somehow to be able to define a procedure with the same name as another one, that takes completely different arguments and may in fact do something completely different, without so much as a trait or a type class to tie the two together. This is something you have to be more careful with, to ensure that you don’t end up over-overloaded and forgetting what types you’re even using at any given point. It can come in handy in limited circumstances.
Useful metaprogramming macros
Although I don’t have as much experience with metaprogramming, and I think that
going overboard with it is a common issue, having it available can be nice for
simple tasks. For example, cascade
is
implemented as a macro, while it would probably need to be implemented in other
languages as a built-in feature. I look forward to writing a few hygienic
templates to get a feel for how metaprogramming can make my code less
repetitive and more idiomatic. The fact that Nim has this feature built-in
makes me feel like a real Lisp hacker of yore.
Handling mutability and side effects
This is one of the areas where most “mainstream” languages have a major blind
spot. It’s not enough to put in const
declarations (though Nim does have
those) and call it a day. Keeping the impure parts of the program isolated as
much as possible from the immutable and purely functional parts is vital. Nim
gives us the tools to do so. The compiler will check for impure side effects
when you attach the noSideEffect
pragma to a procedure, and there’s even
syntactic sugar for using it by declaring a func
instead of a proc
.
A const
is only for values known at compile time. If we want immutable
variables that are initialized at some point during execution, we turn to let
statements, which share the same syntax as the var
statement for declaring
new mutable variables. In this way, the distinction between the two kinds of
variables is made clear, and it’s easy to use mutable ones only when needed.
This is more common in other languages than having a side effect checker, but
it’s still overlooked and not a built-in feature in Python or JavaScript!
The inevitable comparison
It’s time that I address the elephant in the room. Rust is a language that has consistently gathered momentum over the past few years. In addition to most of the language features discussed previously, it has innovative security-related features such as borrow checking, which makes those who keep up with this kind of thing regard it as the rightful successor to C for systems programming. It can also be compiled to WebAssembly, similarly to how the Nim compiler has a built-in JavaScript backend. The design and implementation of Rust is indeed an impressive accomplishment. However, I am hesitant to embrace it fully for a couple of reasons.
Manual memory management is already annoying, and it’s even more annoying when
you have to wrestle with a borrow checker. Nim has a soft realtime garbage
collector by default, which is more than enough of a performance edge for most
applications. It actually has multiple GC
strategies you can select with a compiler
flag, including ARC for hard realtime and “none” for manual deallocation, if
you really need that. There is also a distinction between raw pointers and
traced references (analogous to smart pointers in C++), and dereferencing a
nil
pointer will result in an unrecoverable runtime error. Rust makes it
impossible to do that unless you use unsafe
, and although null pointers are a
notorious kind of error which everyone wants to get rid of, the same
constraints that Rust uses to prevent it also make everything more verbose. For
practical purposes, reading and writing Nim code is generally easier than Rust,
and performance-wise they are in the same ballpark.
Then there are the meta-issues with Rust. Until recently, Mozilla was pretty much fully responsible for developing and maintaining the language. It has now been handed over to a foundation with several other big tech companies as members. That speaks to my perception of Rust as potentially becoming Java-esque, as in corporate and commoditized. Aside from that, the language is still relatively unstable even after the 1.0 release, and many of the implementation details in the standard library are questionable. I hate thinking about boxed versus unboxed types.
There’s undoubtedly a lot of hype around Rust, which can lead to unrealistic expectations and disappointment. It can be considered a solid choice for systems programming and security-critical components. However, when it comes to the kinds of programs I’m most interested in writing nowadays, namely games, Rust falls short as a driver of creativity and Nim actually seems more promising. All I want to do is leave popularity contests behind and start having fun with programming again! Whatever language you prefer, fun is all too often left out.