loading...

Nim v. Rust

aachh profile image Antoni Chmiela Updated on ・10 min read

Hey! This is my first ever DEV post. Tell me if you liked it and if you'd like to read more stuff like this! 😄

So, Rust. It's a language that has a lot of users, including some really big companies. Its community is really vibrant. And it has been the most loved language on Stack Overflow for 5 years in a row now, I think.
It's been used to make async stuff, gamedev stuff, browser engines, dinosaurs, other programming languages, heck, even operating systems.
It's been praised for its ownership system which makes it easy to maintain memory safety without a garbage collector, its package system which has a crate (Rust package) for virtually anything, and safe concurrency.

Listen; if that's not a successful project, I don't know what is.

But, from the darkness, another competitor emerges [1], with not that many users. It provides a set of features very similar to Rust's, is just a bit more readable, and it even compiles to JS! (of course the most precious feature 🙄)
The language I'm talking about is obviously Scratch. [2]
No, wait. Scratch that.
Nim.
In contrast to Rust it's garbage-collected. It does hand you some unsafe possibilities though (Rust does too), so you can e.g. get a raw pointer to a value and manage memory yourself. While concurrency and networking aren't as robust as in Rust (they exist and are possible, but just aren't as mature), Nim provides you with a more flexible and readable (IMO) macro system which lets you go around directly modifying the AST instead of just metaprogramming. Some more interesting features are:

  • message passing (through UFCS),
  • algebraic data types (this one is really neat),
  • a decent package manager, Nimble,
  • stropping, which means you can name variables with keywords,
  • super easy interfacing with C APIs, as the pipeline for running Nim code is "compile Nim to C → compile C,"
  • ridiculously high speed and efficiency. I mean, it wins with LuaJIT in benchmarks, and by some margin. It even wins with Rust, both in memory, time of execution and time of compilation!
  • and many more.

Maybe it hasn't been used as extensively as Rust, but it's had its share of applications. One of the creators wrote a small kernel, there's a sweet JS-less Twitter client, there's even a sharding client for Ethereum.

Again, if that's not a successful project...
it means Rust isn't either. Why? Because I think the languages have equal potential.

Why Nim isn't used as often as Rust

Okay, I guess anyone with at least a little bit of knowledge on the subject will be able to tell the main reason people aren't using Nim as much as Rust is

  1. popularity. Rust is actively "promoted" by Mozilla, as it's there where it's mainly developed, while Nim is a fully independent project that stands on the shoulders of people / entities that sponsor it and contribute to it. As such, it may not have reached the ears of enough people to be used by a significant part of them. Still, even if it did, most people probably didn't care much about it, because of some of the following reasons:
  2. Design, and what I mean by it is how flexible the ideal of the language is to many software design patterns. For example, IMO Rust strongly encourages you to create structs for most of your reusable components and describe behaviours for them, in the form of functions with a Self reference argument. If you need to establish traits—the word just comes naturally—for many kinds of components, create a trait and implement it for your chosen structures. On the other hand, Nim is… very ambiguous. At least in my opinion (just go ahead and assume everything in here is my opinion!). When writing Nim I will often have trouble picturing the layout of the project in my head. What should I use? procs solely? Maybe dabble in OO? I'm never sure, and therefore it slows down my development process by some margin. But it kind of aligns, taking into account Rust's and Nim's different
  3. syntax. Now, this is weird, but it seems people have gone full circle and want more "boring" languages nowadays. We started with C, then the revolutionary Python emerged, and we're here, with Rust, Deno [3] and TypeScript. I think that's good, but it shadows over languages like Nim, where the syntax is quite friendly. It makes for a language perfectly suited for beginners, but a beginner will probably start his programming journey with Python or JavaScript, because that was what first popped up in his DuckDuckGo. Nim is, in my opinion, a very good language, but it didn't appear at the right time, whereas Rust did. I bet you didn't even know that Nim is actually older than Rust. The issue is that Rust's v1.0 came out in 2015, and Nim went stable only last year. Why is that? No-one wants a "friendly" language anymore. People are starting to look more into performance and less into "accessibility." But I hear you saying: "But you said Nim is more performant than Rust!" Yes, but most people don't strive to be literally as fast as they can or else. Most of the time "fast enough" does it for them, and Rust is even more than that. You can actually see this shining through in the 2018 Nim Community Survey. The top reason for why programmers don't use Nim was generalised as "not sufficiently better than other languages." People prefer better tooling, tested-in-combat solutions and bigger communities.

Examples [4]

In case you don't know these languages very well, I'll give you some examples here, so you can see the general idea of what they feel like.

Let's start really EZPZ: the simplest recursive factorial function.
Here it is in Rust:

fn factorial(n: usize) -> usize {
    if n == 0 {
        1
    } else {
        n * factorial(n - 1)
    }
}

You can already see some interesting stuff sticking out: You don't need to write an explicit return keyword in order to return a value: if you supply the function with an expression (statement, but without semicolon) at the end, it will return the value of that expression. May seem weird at first, but it does help readability in bigger codebases.
Okay, now here is the function (procedure, rather) in Nim:

proc factorial(n: int): int {. noSideEffects .} =
    if n == 0:
        result = 1
    else:
        result = n * factorial(n - 1)

Nim is (amongst other paradigms) a procedural language, which means it greatly prefers it if you group code using procedures. A procedure is like a function, but it's generally not supposed to return anything. That's why there's no return keyword in Nim.
Another rare and interesting feature of Nim are pragmas—ways to tell the Nim compiler in what way to compile stuff. The pragma we used here, noSideEffects, is used to mark a proc (or iterator) to have no side effects. ...You guessed right! This is a way to mark a pure function in Nim.
There's actually shorthand for that though—writing func instead of proc with the pragma. That is, if you use func, you don't need to type the pragma. This means the code could look like this:

func factorial(n: int): int =
    if n == 0:
        result = 1
    else:
        result = n * factorial(n - 1)

Pragmas can actually be very useful—for example, there's the deprecated pragma which marks stuff as deprecated. You can even add a message which will be used to warn developers working with your library.
There's a lot of them, be sure to check them out if you're interested!

(Of course, you need to be weary of types as with typed languages. Integer types aren't something that particularly fascinate me though. I want syntax!)

Let's look at some more examples.

This program takes a number and iterates from 0 to that number, outputting something each time:

use std::io;

fn main() -> io::Result<()> {
    let mut input = String::new();
    io::stdin().read_line(&mut input)?;
    let number = input.trim().parse::<u32>().unwrap();
    for i in 0..number {
        println!("Saying hi for the {} time!", i + 1);
    }
    Ok(())
}

Now we're talking code! There's a lot happening here if you don't know Rust. First of all, we say that we're bringing the module io into scope with the use statement. We set the return type of main as Result<()>, which is a cool feature of Rust that lets you handle errors effortlessly.
We get a new String in the 4th line, and then, in the 5th, read a line of input to it. We do that using a mutable reference. Why? If we didn't include that mut, the compiler would say that you're passing an immutable reference to a value you want to mutate. 🤷🏻‍♂️ If you didn't include the whole &mut
That's the magic of Rust—ownership. By passing a mutable reference to a function you let it borrow the value and do what it wants with it. If you passed it only as a value, you'd completely hand over ownership to the function. That's bad Rust code. Read more on it here.
If there's an error during getting the input, we have to handle it! The question mark at the end takes care of that—it's shorthand for the expect function, which "expects" an error and tidily panics with it.
When we finally got the input, the last thing we have to do is cast it to a number. We first trim the input to get rid of any whitespace, then parse it into an unsigned 32-bit integer; we specified that with the turbofish operator (::<>). After that we unwrap the value, because parse returns a Result. If what we unwrap is a value, it gets assigned to number.
So we got the number out of the way. Now we can loop and output each iteration. The 0..number construct lets you easily construct iterating ranges. At the end we return Ok(()), because we need to hold on to our promise regarding main's return type. Ok symbolises success in Rust.

We're finished! Let's check out Nim now.

var number = parseInt(readLine(stdin))
for i in 0..number:
    echo "Saying hi for the ", number, " time!"

Phew! This is much, much smaller. There's no need to specify types, you can if you want to though. No explicit error handling. Everything's comedically simple.
I talked about the Uniform Function Call Syntax earlier—we could use that here and change that parseInt into something like this:

var number = stdin.readLine().parseInt()

In this case (like in all of them) the first parameter of a function can be treated as an object to which you pass a message, in a Smalltalk-like manner.

So we got some basic IO out of the way. Now the last, actually real-world example—a baby-simple server. I will use the Actix Web crate with Rust and Jester with Nim.

This is how it could look like in Rust:

use actix_web::{web, App, HttpResponse, HttpServer, Responder};

async fn handler() -> impl Responder {
    HttpResponse::Ok().body("Hello from Rust!")
}

#[actix_rt:main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .route("/", web::get().to(handler);
    )
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

There's actually not that much happening here. First we bring all the things we need into scope again. Then we create a function that will serve as a request handler later. The impl Responder return type means "something that implements the trait Responder." Note the async capabilities Rust gives you.
In main we just create a new HttpServer and inside that, a new App. The || {} syntax is a closure, just like () => {} in JS. Later we "bind" the server to localhost (and optionally handle errors) and run it.
Actix became really popular because of its high speed, and that's partly due the amount of stuff you have to do yourself when working with it. I guess you can see that in this example.

Okay, now for Nim with Jester:

import jester

routes:
    get "/":
        resp "Hello world!"

Again, Nim just knocks Rust out of the park readability-wise. Jester actually provides a small DSL (Domain-Specific Language, like a small language designed to do a specific thing) for handling your routes. That definitely makes coding servers in Nim way easier to wrap your head around.

Conclusion

Frankly, the only conclusion I'm able to draw from this is that Nim and Rust are different languages. They simply can't be compared because they're suited for and strive to different things and goals. There isn't a "better" language out of the two of them.

My opinion

I like both Rust and Nim. I think they're both great languages which have an insanely huge amount of potential that's just waiting to be used in awesome software. Which one would I pick in a project? Depends on what the project would be. If I was building a fail-safe, scalable backend, I'd probably choose Rust, because it's simply better tooled for that kind of job. If I was building a language VM, I'd choose either Rust or Nim—both of them are suited for that. If I was building a compiler that would focus on speed, I'd probably choose Nim, because it's really easy to interface with C, which would give me C's speed and advantages at the cost of Nim's simple syntax.

What about you? Do you have a favorite? Tune in to the discussion below!


[1] → Yes, I'm a Dunkey fan.
[2] → No joke; Scratch can compile to JavaScript.
[3] → I know Deno is a runtime, not a language, but you know what I mean.
[4] → Note that these aren't benchmarks, but arbitrary examples of the languages. They're supposed to introduce a reader that is new to them and generally show how you can do things in them. This is not a better-worse comparison. Written by me. ✌🏻

Posted on May 19 by:

aachh profile

Antoni Chmiela

@aachh

Working on something really cool right now! (ノ´ヮ´)ノ*:・゚✧

Discussion

markdown guide
 

Other differences can be:

  • Nim has inheritance, Rust has no Inheritance.
  • Nim has template, Rust do not have that.
  • Nim can generate Hardened Binaries easily, Rust can not (at least as of today, in proper way).
  • Nim can generate identifier names with backticks (like Kotlin), Rust I can not find a way to do it easily.
  • Rust has better Marketing, but enforces Trademarks on name and logo.

Lots of Rust dev still depend on NodeJS to do Frontend stuff (I know Rust can do WASM),
but still for some reason, a lot of people still depend on NodeJS and TypeScript,
if thats a Pro or Con, is opinionated, but I prefer to not fight with node_modules, etc.

More idiomatic Nim can be:

func factorial(n: int): int =
    if n == 0:
        result = 1
    else:
        result = n * factorial(n - 1)

Mainly the return type, I would add {.inline.} if was my code :P

Good comparison, nice post, I think you have more experience with Rust, both awesome languages.

 

Hey! Thanks for replying.
I do agree, the other differences you pointed out are key. I actually mentioned the identifier with backticks trick as stropping, turns out that's what it's officially called.
About the func vs proc—I don't know how I could forget about that. I'll actually edit the post for this.
I actually do not have more experience with Rust; I know both of them moderately well, but know a lot of stuff about them.
Really, thanks for replying, it means a lot!