Rust from a JavaScript perspective

 A tongue in cheek walk-through

I’ve been having a lot of fun using Rust for writing small tools. My day to day work involves a ton of JavaScript and Rust provides a familiar feeling, so trying out Rust was an easy decision to make. But at the same time, actually doing meaningful work in Rust requires a lot of rethinking on how to structure and reason about your code. The compiler – true to its call – is merciless; yet, for some reason, it emerges quite a pleasure in tinkering your code to make it so that it – finally! – compiles.

In this post, I am documenting – albeit in a bit funny way – some thoughts in my journey so far in Rust land, coming from the viewpoint of a hardcore JavaScript enthusiast.

The good news

Modern Rust *appears* pretty similar to modern JavaScript. You declare your variables with let, your functions look pretty similar, types aren’t a stranger because of TypeScript , there’s async/await and overall it exudes a sense of familiarity.

The bad news

Unfortunately, the good news section ends pretty fast.  The core of the issue isn’t  syntax, but the way Rust reasons about the internals of your program. In high-level languages, you get cushy abstractions that shield you from the way computers work. And that makes perfect sense; if your goal is to commute to your workplace, you just need to know how to drive the car  – you don’t have to fiddle with the internals of its combustion engine. In contrast, on low-level languages you get the bolts and screws and you have to be a car mechanics to drive to the grocery store.

Each approach has its own set of advantages and disadvantages; for that reason they are mostly used in different problem domains.  Rust aims to sit right in the middle. It gives you access to all the raw facilities while also providing the lucidity and ease of high level abstractions. But – and there’s always that but – it needs you, the developer, to pay a price for this: you must learn a new way of reasoning about your program.

Let there be memory management

A computer program relies on reading and writing values in memory. There’s no way around it, just like there’s no way to go about cooking without ingredients. Therefore, somehow we have to procure the ingredients, slice them in the right quantities, throw them into the pan by the proper order and eventually take care of the mess in the kitchen after we’re done.

High-level languages are like dear parents. They are there to patiently clean up for you, so that you can perform your …art without getting your hands dirty.  Mindfully, they provide you with a nice & helpful Sancho Panza – lovingly dubbed as “the garbage collector” – for some much needed backup as you fiercely engage those aggressive windmills.

When it comes down to memory management, Rust is like “meh – real chefs clean up their own trash”. And there is good reason for this, because a garbage collector comes with its own set of esoteric issues that can hurt you when you least expect it. At the same time though, Rust draws from past experience of other languages and accepts that forcing the programmer do the memory management is as wise as commissioning Douglas Adams to write the “Starship Titanic”.

In order to get around both humans and too-sophisticated-for-our-own-good code, Rust came up with a new scheme. It can be summed up as “We are all Lannisters now”.

A rose by any other name would smell as sweet

… as rightly so says Shakespeare. Yet, what about the …not so sweet smells? Does the saying stand true for the negative case? Considering the long tradition in humankind’s use of euphemisms, we can be relatively sure that it is not so.

A 55-meter long ship somewhere in the Pacific, posing as a tiny island to escape detection in WWII; it actually worked.

Rust playfully engages in the time-honored art of misdirection by words.  Prominent in its literature is the concept of “ownership”.  Yet in Rust, “ownership” has no big benefits for the owner. Instead, “ownership” means that some relative passed down their debt to you; and now you own that debt. And – make no mistake – there’s no defaulting on debts here.

In the Rust’s Westeros continent (dare I say, Westerust ?), there are tiny, small, big and huge fiefdoms. The twist here is that each and every fief is occupied by Lannisters. Fiefs do their own things internally and when they need “goods” from the outside, they will incur a debt in order to obtain said goods. In the end, the debt must be paid back to the Westeros Gods. Rust is the draconian Queen of this world – it will oversee everything from high up to make sure the Gods get their dues.

In Rust, every scope is its own fief. Variables (memory) that is procured from the system and used inside the scope stays – like secrets in Vegas – only inside that scope. If a scope is done and no longer serves a purpose, the memory is returned to the system. The compiler makes sure of that because it transparently injects code in your hand-crafted fiefs that does exactly that. So it’s impossible for your scopes to escape their fate. This is the iron rule of Rust (hurrah for the levels of irony).

But can it run Crysis?

Let’s see how it works.

We have 2 scopes here. The outer one from main and an explicit inner one for demonstration. Rust ownership here works like this:

  1. main owns a and b
  2. a wants to work with the inner scope, so main transfers ownership of a to  inner scope
  3. inner scope does its thing with a and then completes.
  4. Rust’s hidden code drops a.
  5. main does its thing with b and completes.
  6. Rust drops b.

Notice that the debt of ownership is paid back to the system, not to the scope it  originated from. Ownership of a does not return to main scope.

But wait, this sounds very dangerous. What if we had the following code?

Here, main scope wants to use a again, but we said that Rust has already dropped a when the inner scope ended.

Won’t the program just crash and burn when it reaches that point in execution?

Yes, it will. But as the Spartans responded to Alexander’s father, King Philip II of Macedon: IF it reaches to that point.

The Ultimate Bureaucrat

The Rust compiler is a proud disciple in the tradition of Legalism, so much so that Han Fei-tzu would have exuberantly outlawed all other languages.

Nothing happens in Rust-land unless the Compiler stamps it with its seal of approval. The Compiler will thoroughly check everything and evaluate if the program is safe to run. Only if satisfied, will it ever produce an executable.

In our case, it realized that our debt handling skills were subpar, and our request for a binary got refused.

It is the role of the programmer to learn what the Rust Law is and make sure that it is adhered. All its ins and outs, all its quirks, all its assumptions. Otherwise, the Rust Compiler will yell at us. On the other hand, the Rust team has been trying to make things accessible by creating a ton of syntactical sugar and – most importantly – legible error messages. That, coupled with decent documentation and a great community makes working with the language fun.

The Rust RPG

In the Rust continent, variables are the players. Players must belong to some class – a mage, a priest, a struct. Moreover, each player may have different equipment. That makes sense; you could have two priests, one with a staff and one with a wand, right?

Remembers that dbg!() from before? That’s a macro, a rough equivalent of JavaScript’s console.log. Let’s create our own Typed variable and log it.

We created a struct, which is basically a Type. Then we created an object of that Type. Finally, we asked to log that object.

Nope. Our player is so much of a noob that it does not even have the ability to provide debug info. Really! Amazing…

The key point here is that your handcrafted variables all start as Level 1 peons, with no equipment. And here is where the equipment (Traits, in Rust lingo) come into play.

Let us press F.

This time, it works. The only difference is the line at the top. Here, we equip Noob with the Debug trait. Now, our player is eligible to be logged – what a milestone!

Rust has a ton of equipment. Some more prevalent that others. And, as expected, it lets you forge new ones by using your very own designs.

Some Traits can be generated automatically by the compiler for us. This is what happened here – the compiler was able to relieve us from trivial work. Other times, the actual implementation is left to you. You want a graphite armor for your mage? Not a problem, certainly is doable but you have to provide the code on how it actually works.

Traits are deeply embedded in the fabric of Rust. Let us circle back to the ownership example that blew up  earlier. Reading the error message carefully, we notice that the compilers explains to us that the variable’s ownership had to be `moved` because a String does not implement that Trait Copy.  Otherwise, the compiler – wise as it is – would have made a copy of it instead of a move.

The Copy trait means that you take a section of memory and you memcpy it somewhere else, operating directly on bytes.

Ok, so String doesn’t have a Copy trait, do we just need to tell the compiler to provide it with one? Unfortunately, no. Copy is too low level for a String to safely use it. The compiler knows that is so, so we get nothing. Of course, Rust wouldn’t be a useful language if the story ended there. There’s a more explicit Trait that does almost the same thing – Clone, as it’s called. String does have the Clone trait, so we simply want to use that instead of Copy.

We will adjust the code a bit to look like this:

What happens here is that the compiler sees that we want to use a inside the `inner scope` but now it also sees that we can do our work perfectly fine with a clone of it instead of the actual a. So we have the following:

  1. a is owned by main
  2. a.clone is created and borrowed to inner scope
  3. inner scope does its thing and completes
  4. Rust drops a.clone
  5. main uses a with no problem because a was always owned by it.

Beautiful. Of course, that’s not the only way to solve this particular issue but it ties nicely with our little exploration of ownership and Traits.

Before wrapping up this post, there’s one more thing we should touch upon a bit. We talked about ownership and how it relates to scope, but truth is this was just a simplification. Rust employs the concept of Lifetimes to keep proper track of ownership. It just happens that most of the time, lifetimes and scopes coincide. Sometimes though, the compiler needs help. Therefore we are allowed to work directly with those lifetimes – in some cases we must.

FIN?

Absolutely not! If Rust is an iceberg, this can’t be considered even the tip. If Rust is a cake, this isn’t even the glossing. If Rust is a cryptocurrency, this isn’t even a satoshi. But I am clearly not conjuring inspirational metaphors today, so let’s let it rest.

In my experience, learning Rust is fun. There is a steep curve but at the same time, the amount of complexity is “just right” for the value you get by investing your time in it. I will definitely be continuing my journey with Rust!

 

 

9 thoughts on “Rust from a JavaScript perspective”

  1. Thoroughly enjoyed the article, very well written! As a newcomer to _Westerust_ myself, it captures all my emotions nicely!

  2. Great article, really love your writing style. Could you clarify this for me, you wrote “a.clone is created and borrowed to inner scope”, but isn’t a.clone owned by the inner scope now, hence why it’s dropped when the inner scope ends. If it was just borrowed, wouldn’t it outlive the inner scope? Thanks.

    1. The clone is owned by the inner scope. Here I used the word “borrow” in the non-technical context of debt. The proper term in Rust lingo would be “move” (of ownership).

      In retrospect, it is a bit of an unfortunate choice of wording because the word “borrow” also exists as a technical Rust term (but means something different than what I was discussing there). In Rust lingo, to borrow a value is to pass a reference (pointer) of it.

      let a:String = "Bar".to_string();
      let b = a.clone();

      foo(a, &b);

      Here, ownership of `a` is moved into `foo` but the second parameter only passes a reference of `b` and so `b` is still owned by the outer scope. In Rust lingo, `b` has been borrowed to `foo`.

Leave a Reply

Your email address will not be published. Required fields are marked *