DEV Community

aachh
aachh

Posted on • Updated on

Nim v. Rust

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. [edit] 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 performance. I mean, look at the benchmarks! It even wins with Rust, both in memory efficiency, 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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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. There is a return keyword, but often when writing Nim code you won't have to use it. Most of the time you'll use the result procvar which stores the result of the procedure. It's handy for when the result is supposed to be a collection, like a seq or Table. You can gradually append and delete items as you like without defining another variable.
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)
Enter fullscreen mode Exit fullscreen mode

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(())
}
Enter fullscreen mode Exit fullscreen mode

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.

import strutils

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

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()
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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!"
Enter fullscreen mode Exit fullscreen mode

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. âœŒđŸ»
[edit] → July edit: thanks to Shirley Quirk in the comments who pointed out Nim is soon switching to Objective-C-esque automatic reference counting as its default memory management option! Super excited for the future of Nim!

Top comments (7)

Collapse
 
juancarlospaco profile image
Juan Carlos • Edited

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)
Enter fullscreen mode Exit fullscreen mode

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.

Collapse
 
aachh profile image
aachh

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!

Collapse
 
chayimfriedman2 profile image
Chayim Friedman • Edited

Some notes:

First, you don't need the turbofish in your Rust. Rust's type inference is really powerful, and in the following code:

use std::io;

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

It still can understand that number is an integer (due to it being used in the range).

Second, Rust can compile to JS, too 😃. This is thanks to LLVM - any language compiled to LLVM IR can be converted to asm.js or WebAssembly.

Second, the ? operator is not a shorthand for .expect() nor .unwrap(); it's desugared like this:

fn f() -> ReturnType {
    let _value = expr?;
}
// Desugared into:
fn f() -> ReturnType {
    let _value = match ::std::ops::Try::into_result(expr) {
        Ok(v) => v,
        Err(e) => return ::std::ops::Try::from_error(From::from(e)),
    }
}
Enter fullscreen mode Exit fullscreen mode

Third. The typing "boilerplate" the Rust code has compared to Nim in the example is just due to the ability to omit main() in Nim. Of course, in real-world program there isn't a difference.

Fourth. Rust has really powerful macro system, too. You've chosen to use a library that doesn't use that, and that's OK. There are people that prefers this style (I too, in most cases). But even if there aren't libraries that allows you to write code like your Nim server, this is clearly possible.

Fifth. I don't know Nim much, but AFAIK it has exceptions. Rust could have exceptions, too (in fact, you can simulate them), but there is a reason it chose not to do so. Exceptions are really, really bad in production. Decades of engineering proved that. They're really "you can't without them, but you also can't with them".

Sixth. Not having GC is really an advantage for Rust. Reference counting bring Nim closer to Rust (it also has some form of it), and can be better for system programming (because of the GC's latency), but still adds overhead. For each store. Rust, not only it can eliminate the need in most of the cases due to compiler's excessive knowledge about the program, also idiomatic Rust almost doesn't get to this situation because of the ownership and borrows.

Seventh. The biggest advantage of Rust is its safety. Rust wasn't created to answer the need for a modern system programming language (although it answers it too), but to answer the need of a safe system, concurrent programming languages. Again, I don't know Nim, but from what I've read about it, it doesn't deal with this problem at all.

Eighth. Rust isn't Python, and it's not good for small programs. The advantages of an intransigent compiler become obvious as your project grows. Ask the folks at Microsoft about the need in TypeScript and the ever growing ecosystem around it - static typing over a dynamically typed language!

Ninth. Syntax is really matter of preference, and one can claim that C's syntax is the worst invented. But anyway, the people that program in Rust are not students; they're people working at REALLY big projects. Those already have experience with C-like languages. The fact that almost all (if not all) mainstream languages use C-like syntax is not accidental.

Tenth and last, but certainly not least. Rust is more mature. With a big difference. The users are more, the packages are more, and the ecosystem is enough to build real programs. The fact that several big companies use Rust in their big projects at least not disturb that. Nim ecosystem, although older, is not there. It can get there, eventually, but if you ask for my guess - it won't. There isn't enough space for many system languages, and this area in the industry is changing very slowly. Nim could be a great language to replace, say, Java and C#, but it doesn't aim there. And I think there is a room for one, and only one, language to replace C/C++.

Collapse
 
ap2008 profile image
AP2008

The factorial nim program can be rewritten as (without the result var and return proc):

func factorial(n: int): int=
  if n == 0:
    1
  else:
    n * factorial(n-1)
Enter fullscreen mode Exit fullscreen mode
Collapse
 
aachh profile image
aachh

True!

Collapse
 
shirleyquirk profile image
shirleyquirk

Just want to add: Nim now has automatic reference counting (Ă  la objective C) as a memory management option--soon to be the default. So no garbage collector needed any more!

Collapse
 
aachh profile image
aachh

I didn't know that, that's sick! Can't wait! 🎉