HAE Dot Logogram Hector A. Escobedo 2022 HAE Dot Logogram

Nim: my first impressions

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.