Nim is a programming language I had been eager to try for a while, ever since I started looking more deeply into C++ alternatives (beyond Rust).

I remember it immediately stuck out to me for having seemingly Python-like syntax while promising high performance as a systems language. I am constantly thinking about ways that programming languages can be made more ergonomic, so naturally, I get very excited when I come across a language that actually strays from typical C-like syntax. Right now, I’m very excited to see how Mojo develops!

I was also intruiged by how the language can be garbage collected yet also supports custom allocation and memory management strategies. Although I’m not experienced with things like custom allocators, I do primarily come from a C++ background, and so I’m not too keen on using garbage collected languages - it’s why I’m yet to really give Go a go.

Having recently finished an implementation of Pong in Odin , I was left rather disappointed and was craving something more interesting. And so I pulled up my list of languages to try out and saw Nim towards the top of the list. As such, I decided to program another classic game, Snake , with the help of Raylib . In particular, I used Naylib , a wrapping for Nim that conforms to its style.

In this article, I’ll to go over my thoughts about the language after having used it for a whopping 4 hours, and explain why I probably won’t be using it again anytime soon!

First Impressions

Before I got started with my Snake project, I first installed the Nim compiler and toolchain - including its package manager, Nimble - and compiled a hello world program as I typically do when I install a new language:

echo "Hello from Arch Linux Hyprland!"

How succinct! Although I must say, I don’t get the appeal of using echo over print. Maybe it’s to help emphasise the Bash-like command syntax. I am a fan of said syntax though, especially coming from Haskell . It’s just as clean as it gets really.

Entry Point

I don’t feel particularly strong on the absence of an explicit main function. Conventionally, main is handy for finding the entry point of a program, of course, but it’s not that much more difficult to navigate without it. Plus, if you do feel strongly about it, you can always define a custom entry point, even if just for some organisation.

proc main() =
    echo "echo"

main()

The lack of main also means you need to read command-line arguments via the std/os module, and you likewise can’t just return an exit code like you would in C. Thing is, these conventions are seemingly being abandoned anyway, with both Rust and Zig treating main as void and processing command-line arguments via library functions. Plus, the entry point being the top of the file itself is something Python programmers will already be familiar with, so really it’s leaving one convention for another.

Procedures vs Functions

What many will be less familiar with, however, is the proc keyword - “procedure” rather than “function”. Well, except Nim does have functions, but they are reserved for the more mathematical concept of a pure function that has no side effects . I first saw proc used in Odin, and I’m glad that Nim also makes the distinction between a process and a function.

That being said, I am weary of how pure functions can get in the way of debugging. The thing that made debugging logic errors in Haskell so hellish was the inability to print without changing the function’s signature from pure to IO (). Thankfully, Nim actually does have a solution in the form of debugEcho - an echo treated by the compiler as if it has no side effects.

func foo() =
    echo "caw" # Compile error
    23
    
func bar() =
    debugEcho "caw" # A-OK
    67

It comes across as a bit hacky, but I appreciate it all the same. Perhaps all the hype around pure functions will one day lead to good debugging for them becoming mainstream, preferably built into compilers.

Implicit Result

You will also notice that Nim supports implicit returns when you end a procedure with an expression, like Rust. Well, this is partially true, as Nim also has a feature that I actually also thought of myself a while ago, and I was surprised to actually see implemented in a popular language:

import std/math

func quadratic(a, b, c: float64): float64 =
    result = sqrt(pow(b, 2) - 4 * a * c)
    result += b
    result /= 2 * a

echo quadratic(1, 5, 6)

Here, result is an implicit variable for the result of a non-void procedure. And it does genuinely have its uses - for example, here’s how I used it in a “constructor”:

proc newSnake(startPos: Vector2): Snake =
    result.head = addr result.body[0]
    result.length = 1
    result.head.position = startPos
    result.head.direction = right

Keywords vs Symbols

Notice how I get the address of result.body[0] via the addr keyword rather than a symbol like &. The same is true for pointer types being denoted via ptr rather than a * or ^. And yet, in order to reference a pointer, you use [] rather than a keyword…

var a: int = 23
var b: ptr int = addr a
b[] += 3

echo a
echo b[]

…something which I find rather strange. It is consistent with indexed dereference syntax for arrays though, if you think of arrays as pointers. Issue is, arrays aren’t just pointers, and [i] semantically means “get the element at this index of this list container”, which is why it is used for more than just pointers in most languages.

And weirdly (and I can’t wrap my head aroud this), Nim then uses the * to… denote members as public within modules.

type
  Person* = object # Person is visible to other modules
    name*: string  # name is visible to other modules accessing Person

I’m not sure how this is better than public, let alone the more concise pub. I didn’t end up splitting my project into modules though, so maybe there’s some nuance I’ve missed.

Nim also replaces && with and, || with or, and ! with not, although still uses != (!+=) for “not equals”. I am a fan of this, and have even considered using this convention in my C++ code since C++ does in fact support operator alternatives , including not_eq for !=. Whether or not to use != is quite a dilemma since, on one hand, cool fonts (like on this blog) can change it to a long equals with a slash through the middle, but it will normally be displayed as a !+= which may leave a programmer wondering why ! is used to mean “not”, and creates friction if the ! operator is to be repurposed for something else.

One such solution is to simply not support it and instead require expressions like not (5 == 3), but that can be quite cumbersome. You could also instead use /= as seen in Haskell, but this conflicts with divide-equals, and would therefore also impact +=, -=, and *=. Two other symbols that could actually work are \= and =/= (=+!+=), the latter of which is used in Erlang for strict equality.

Generally though, I do like the idea of using keywords over symbols like &&, ||, and !, especially since the words and, or, and not are only 2-3 letters long anyway. Then, I’d finish off by replacing != with =/= or something. I also don’t mind the use of ptr, addr, and ref keywords for similar reasons, but really that comes down to how often you use raw pointers and access addresses in the language. A language like Rust that uses & to denote borrowing should absolutely use a special symbol over a keyword due to the sheer frequency of it use, and that’s a general rule that can be applied to symbols in all languages - if it is extremely common, make it quick-n-easy to type.

Variables, Constants, and… Lets?

Nim supports 3 ways to bind values to names:

var a: int = 3
const b: string = "foo"
let c: float = 4.2

var defines a mutable variable as you would expect, but both const and let define immutable bindings. The difference is that const is analogous to C++’s constexpr in that a const value must be known at compile-time. My main issue with this is its inconsistency when read. a is a var. b is a const. But c is a… let? Rust suffers from the same issue since, although it uses let and let mut, it also supports const for compile-time constants.

Really, I don’t see why there particularly needs to be a distinction, as the compiler should be able to deduce from context whether an immutable binding can be known at compile-time or not - I mean, hell, it literally has to check in order to report an error when you try marking something runtime as const. To my knowledge, Zig is an example that doesn’t make a distinction, yet allows you to force compile-time evaluation via comptime:

comptime const x = foo();

But there is an argument for more explicit contracts being better for communicating programmer intent. In that case, I will shoutout Odin’s syntax for defining compile-time constants, especially as it’s consistent with the language’s syntax for defining procedures, structs, and others:

package main

import "core:fmt"

msg :: "hello"

main :: proc() {
    fmt.println(msg)
}

In general though, I think I’ve come to appreciate and prefer let and let mut over var and const. It means there’s an expectation for everything to be constant unless you define it as mut - the inverse of C++’s situation with its const keyword - and means that if you forget to mark something as mut, you either get told by the compiler or accidentally get the better result anyway. It also just semantically signifies that you can make something mutable later if needed, whereas changing a const to var can feel more… impactful, in a way that’s hard to put into words. You could argue that it’s better for it to feel more impactful and that Rust in general struggles with making bad decisions feel too easy (another example is the ? operator and .unwrap()), but that’s a whole post in of itself, I think.

I will mention though - I find there’s currently a disconnect between how we define bindings in source code and how they actually get mapped in memory, if mapped at all. For example, consider the following snippet:

const X = 5;
println!("{}", X + 3);

Under normal circumstances, there is no way that X here is stored as a binding in memory. Instead, it is certainly made inline at compile-time, and then further used to evaluate X + 3 = 5 + 3 = 8 as well, so 8 (or even "8") just becomes hard-coded into the machine instructions. For reasons I can’t quite define, it feels ill-fitting to refer to X as a “constant”, and maybe inline would be more fitting, although then what if you have a const array that is actually stored in memory at runtime? This is something I want to think more on, but for now, I can say that Odin’s x :: 5 speaks to me for this reason, and may be worth exploring in more depth.

Implicit Namespacing

I just had to mention this briefly, because I love it when languages support the following:

type Direction = enum
    up, down, left, right

proc advance(direction: Direction) =
    case direction
    of    up: snake.position.y -= 1
    of  down: snake.position.y += 1
    of right: snake.position.x += 1
    of  left: snake.position.x -= 1

advance(up) # Direction.up
advance(left) # Direction.left

iirc OCaml supports this (to an extent), and I really wish Rust and other languages did too… I’d imagine the effects on compilation time are minor, meanwhile the ergonomics are insane. The same goes for namespacing in general, as Nim doesn’t require you to prefix imports with the library name. It’s why I was able to use Raylib like so:

while not windowShouldClose():
    beginDrawing()
    clearBackground(Black)

    if isGameOver:
        if isKeyDown(Enter):
            reset()

Rather than something like:

while not rl.windowShouldClose():
    rl.beginDrawing()
    rl.clearBackground(rl.Color.Black)

    if isGameOver:
        if rl.isKeyDown(rl.KeyboardKey.Enter):
            reset()

It seems unimportant at first, but the improvement on readability is honestly more significant than is often acknowledged.

Yes, namespace conflicts are an issue, but the habit of prefixing literally everything with a namespace - even stuff from the standard library - is very much due to C++’s issues with #include. In a language where you can control what you import from a module and even specify your own namespace for it independent of what the module specifies, I don’t believe the habit is necessary. Rust code uses Vec over std::Vec, Zig allows custom namespaces via const whatever = @import("std"), and Go just uses the package name as the qualifier even with its standard library, like with fmt.Println.

Significant Whitespace

On the topic of readability, I have found that I am a big fan of significant whitespace. Consider this code snippet in Nim:

if not isGameOver:
    snake.updateDirection()
    if getTime() - lastTick >= tickRate:
        snake.advance()
        snake.checkCollision()
        if snake.head.position == apple.position:
            apple.goToRandPos()
            snake.grow()
        lastTick = getTime()

    apple.draw()
    snake.draw()

    let scoreStr = $score
    const scoreFontWidth = 60
    drawText(scoreStr, centerTextHorizontal(scoreStr, scoreFontWidth), 40, scoreFontWidth, White)

endDrawing()

And how it would look without significant whitespace:

if not isGameOver {
    snake.updateDirection();
    if getTime() - lastTick >= tickRate {
        snake.advance();
        snake.checkCollision();
        if snake.head.position == apple.position {
            apple.goToRandPos();
            snake.grow();
        }
        lastTick = getTime();
    }

    apple.draw();
    snake.draw();

    let scoreStr = $score;
    const scoreFontWidth = 60;
    drawText(scoreStr, centerTextHorizontal(scoreStr, scoreFontWidth), 40, scoreFontWidth, White);
}

endDrawing();

You may be inclined to disagree, but I personally do not find that the braces {} and semicolons ; help with readability. If anything, they add visual noise and make it harder to read, and make writing code more prone to errors that waste compilation cycles.

You will also notice that despite using braces, we still conventionally use the same indentation; and despite using using semicolons, we still conventionally put statements on the same lines. And that is only by convention, which can of course be ignored.

A key benefit of significant whitespace is that it forces code to be much more uniform, which is something to be appreciated. It’s the reason that many modern languages enforce formatting rules despite them not benefitting the compiler. And while I understand that significant whitespace is more difficult to parse, modern computer hardware should be at the stage where the impact on compilation times is minimal, or so I assume given languages like Python, Haskell, and Nim exist.

However, Nim also opts to force spaces over tabs which I am less happy about. I don’t see same-width indentation being particularly important - I didn’t like it in Haskell either, and my opinion wouldn’t change unless I see examples of where it actually matters for readability. Instead, I’d advocate for enforcing tabs. Really, it’s just an accessibility feature that allows people to personally customise their tab widths when reading anyone’s code.

camelCase

Speaking of spacing, camelCase has grown on me due to its compactness. Initially, especially due to coming from C++, I hated camelCase and much preferred to use snake_case, going out of my way to use it in Unity with C# for a while before realising I was fighting the compiler and causing myself unnecessary aids.

Since then, I’ve come to actually like camelCase simply because it is much more compact than snake_case. This has become particularly true as a result of me trying to use better variable names. For example, go_to_rand_pos() becomes goToRandPos(), and is_game_over becomes isGameOver.

Perhaps you do not see the appeal, but is it not strange that even in languages like C++, programmers opt to use PascalCase for things like user-defined types? You almost never see Whatever_This_Case_Is_Called because it looks quite weird. It also means that the underscore _ can be reserved for something else in identifiers, although I can’t imagine what right now.

The only real annoyance with camelCase is in the less-rare-than-you’d-expect instances where you want to add/remove a word at the start of an identifier, like position to newPosition. It’s minor, but worth mentioning.

Object-Oriented Programming

So far, I’ve had a lot of good things to say about Nim. But unfortunately, this negative is a real deal-breaker for me, and honestly makes me quite sad :(

type Snake = object
    head: ptr SnakePiece
    body: array[0..512, SnakePiece]
    length: uint32
    oldDirection: Direction

proc newSnake(startPos: Vector2): Snake =
    result.head = addr result.body[0]
    result.length = 1
    result.head.position = startPos
    result.head.direction = right

proc grow(self: var Snake) =
    self.length += 1
    self.body[self.length-1].position = Vector2(x: -1000.0, y: -1000.0)
    score += 1

proc draw(self: Snake) =
    for i in 0 .. self.length - 1:
        self.body[i].draw()

Nim supports OOP . In fact, it supports the bad version of OOP as it outright supports inheritance via ref object of:

type
  Person = ref object of RootObj
    name*: string
    age: int
  
  Student = ref object of Person # Student inherits from Person
    id: int

For reference, the “good” version of OOP is akin to what Rust does with type traits and impl.

And yet despite this, Nim has… outright terrible encapsulation!

It goes down the C path of not allowing methods nested within or attached to structs. Instead, it makes use of global dot syntax such that foo.bar() is analogous to bar(foo). Already this is an issue as it means there are two or more ways to do the exact same thing - not good.

proc add(a: int, b: int): int = a + b

echo add(3, 4)
echo 3.add(4)
echo 4.add(3)

And then the language tries to rely solely on this feature! So the only way to actually associate procedures with a struct is by making the type of the first parameter said struct. This means the functions can be defined literally anywhere, making them hard to track - especially because the order of declaration matters in Nim!! This ended up being a big annoyance for me and is the main reason I couldn’t be bothered with splitting my project across multiple source files. Apparently it’s something to do with metaprogramming, but given that other languages cope fine with order-independence, I don’t quite understand why Nim has it be this way.

And then to top it all off, because the only way to get makeshift struct methods is via this dot syntax, you cannot have static struct procedures. That means no Foo::new() or Foo::from(bar), patterns I love in Rust. Instead, static constructors that return a struct object are declared as newFoo() (and only by convention).

I would have much rather have liked to have seen something more akin to Rust’s way of doing it, and I hope I come across a language with Nim/Python-like syntax that does someday. I don’t like the term “OOP” because it has bad connotations, but the core idea of encapsulating not just data but abstraction in objects is so fundamental to how we operate as humans. A language which messes up this feature is doomed to only appeal to C programmers, I fear (ahem Odin).

Final Thoughts

So, as much as I appreciated a lot about Nim, I unfortunately can’t see myself continuing to use it when other options exist. The extent of its fun is limited due to its issues with OOP, and honestly I would probably have a better experience using Python at that point.

What’s more, I tried compiling my Snake game to WASM (since I’m on Arch Linux), but the process turned out to be more struggle than I could be bothered to put up with at the time. It didn’t help that, a) the documentation on Nim is limited compared to other communities, and b) the compiler honestly isn’t great at reporting errors in a way that’s easy to understand.

I will say though, for the short time I did use it, it was fun and quite refreshing trying out Nim, and it helps that I had a clear, simple project in mind that I could use Raylib for. It’s important that new languages are made that deviate from conventional syntax and practices because it allows us to try and test new things. Haskell sure as hell isn’t a lovely language to use (despite what FP purists may try and tell you), but it is jam-packed with so many cool, novel, and useful features that helped lead to languages like Rust and Zig today.

So although I don’t see Nim or Odin competing with the likes of Rust, Zig, and modern C++, I do appreciate them merely for existing. And hey, this was a good excuse for me to make a Snake game. The next time I do something like this, I’ll try harder to compile it to WASM so I can embed it into the article…