How to Become a Rust Super-developer

This is an article and a tutorial about stumbling and failing. It is about trying hard, and giving up - just to start all over again. All for the one goal - becoming the master of coding in Rust.

Have you ever wondered how two developers can be so different? Both write code, but the first one writes code that is scalable, runs forever and has nearly no bugs. The second one struggles to make sense of 99 little bugs, which turn into 100 little bugs once they fix one. Many people say "That's experience and practice", and I think they are right, but at the same time, it is also important to explain which skills a good coder should develop.

There are many ways to improve oneself as a developer and all of them have merit. So here is one way, which I personally think is one of the best to become that first guy.

Content

What is Rust?

Did you ever have the feeling that people around you start to hype about something, but when you check it out, it looks lame or like too much of a hassle?

Just until last year, that's how I felt about Rust. Everyone was like "Hey, look at this programming language! It's so cool!!!", however when I saw the syntax, my head started spinning and I could only think: "Well, yeah..... no. What an ugly syntax. What a roundabout, complicated way to get things done. I can do the same with modern C++ and have more control at the same time."

Believe me, thinking back, it's a shame, and I couldn't have been more wrong. Contrary to what I thought, after doing more research and talking to people actually using it and having more experience than me, I found out that Rust indeed is a language, which is very well thought out. It just works differently from anything I knew.

How it All Started

Clever minds created C++ out of the need to program computers in an easy and comfortable way. At the time, there were single-core machines and whatever didn't perform well would in one to two years time. It is quite well known, though, that today, computers are sped up by adding cores instead of GHz. New processor architectures try to parallelize work. That's not something the old programmers anticipated, and languages, such as C++, were not made with that focus. As such, this legacy is holding back the language.

Graydon Hoare, Mozilla employee, observed this bad trend back in 2006, and decided that it is time for a remedy. He built a tool which is focused on modern hardware architecture and reflects the changes in mindset since certain languages became a thing. Quite aptly, he named the language after a family of deadly fungi, killing useful plants, which are very, very

amazing creatures. Five-lifecycle-phase heteroecious parasites. I mean, that's just crazy. talk about over-engineered for survival

According to Graydon, they can reproduce and work in parallel, distributed, offering great robustness and performance.

Oh, well, and there you have the main goals of a new programming language. Based on the principles of amazing fungi (which are called "Rusts" by the way), the new amazing "Rust" programming language should be

  • Fast,
  • Efficient, and
  • Memory-safe.

Later on, these goals where rephrased several times, and since the 2018 rewrite, the official homepage defines them as:

  • Performance,
  • Reliability, and
  • Productivity.

So, Graydon wanted to tackle a modern problem with biology - a solid idea. While he was at it, he also added all the programming knowledge he gained over the years to his new project.
Others shouldn't make the same mistakes, after all. The result is a sophisticated language, which Mozilla endorses and sponsors since 2009. It started powering exciting new multi-core projects and is one of the programmers' most loved language - according to the StackOverflow 2018 survey - with 78.9% of the votes.

Who is Using Rust?

Rust, having its base in system programming, can be employed in a wide range of fields. However, its original use case is regular application development. Mozilla, of course, is the number one user in this field, creating the Servo browser engine, which is a sandbox for Firefox. By now, large chunks of Servo have been assimilated into Firefox.

In addition to that, it quickly became clear that concurrency in programming has an ideal application in web servers, which have to serve thousands of users a day, while being robust. A lot of security problems emerged over the course of the past years because of memory handling weaknesses, so Rust seems like an ideal fit.

Among others, MaidSafe has made it their goal to create a completely safe network on top of Rust-only architecture. Others, who started rewriting parts of their architecture in fast and safe Rust, include Dropbox, npm, Cloudflare, Atlassian, Threema, and many more.

Outside of big companies, a huge number of exciting projects began to become public. Here's a collection of a few of my favorites, in no particular order:

  • Amethyst, an ECS-based, data-driven game framework and engine for 2D and 3D
  • Redox, a kernel and user-land (complete OS), which already runs on x86_64 hardware
  • Tock, a kernel for ARM (Cortex) embedded processors
  • Firecracker, virtualization technology for your own serverless providing servers
  • exa, a replacement for ls
  • RLSL, Rust to SPIR-V shader compiler
  • WASM, writing binaries for the web in Rust, simple and easy

Taking the Leap and Becoming a Rust developer

At the time of getting in contact with Rust, I was working on a custom engine for my game. I wanted to learn OpenGL and Vulkan, graphics APIs, which are open, cross-platform, and especially Vulkan is the latest and greatest on modern architecture. That's why I spent a lot of time in front of C++, writing code, cursing the new "smart" features and "sane threading" aids.

The idea is as follows - there is a thread pool and a task dispatcher. There are a lot of tasks, which would run and at the same time add tasks to the dispatcher. At some point, the rendering would finish and the main thread would then do its OpenGL thing (while other threads, such as AI would be able to run), or Vulkan commands would be dispatched from everywhere and then rendered to the screen.

This sounded like a good idea to me. Very wow, much scale-able, so multi-core. Sharing data, though, isn't that easy, and C++ has a few surprises up its sleeve. Also, when Vulkan became interesting, threads would have had to start sharing more info. HORROR 😱.

At this point, my idea in the hindsight sounds more like a coupled hell of small functions, which is anything but manageable.

So, I started looking for a remedy. What I found were libraries, for the most part, some good tutorials and books, the ECS architecture... and Rust.

Rust. Again. Always. So, I took some time to actually take a look at the language everyone has been speaking happily about. I don't have any pressure for my project and can do whatever I want. What could I lose?

How I Transitioned From C++ to Rust...

Did you ever give a modern ecosystem a try? NodeJS? GoLang? Dart? The installation is smooth, updating is simple, there are packages or modules which can be pulled in from a central repository with just one line. Rust is no different.

In fact, it's so simple, I can search for "Vulkan" in the central repository and find one to three crates which fit perfectly - or no crates at all. That's what I did, and that's how I found a simple Vulkan wrapper called Vulkano. It promises Vulkan bindings with Rust goodness on top. When going Rust, why not go all in?

The thing with Rust is, it has a great book, describing all the things which make it up. Also, more than one unofficial "From C++ to Rust" guides existed. With all that, how hard could it be? After all, the features of Rust are just for making sure that I write correct code, right? And look at this easy example!

fn main() {
    let greetings = ["Hello", "Hola", "Bonjour",
                     "Ciao", "こんにちは", "안녕하세요",
                     "Cześć", "Olá", "Здравствуйте",
                     "Chào bạn", "您好", "Hallo",
                     "Hej", "Ahoj", "سلام","สวัสดี"];

    for (num, greeting) in greetings.iter().enumerate() {
        print!("{} : ", greeting);
        match num {
            0 =>  println!("This code is editable and runnable!"),
            1 =>  println!("¡Este código es editable y ejecutable!"),
            2 =>  println!("Ce code est modifiable et exécutable !"),
            3 =>  println!("Questo codice è modificabile ed eseguibile!"),
            4 =>  println!("このコードは編集して実行出来ます!"),
            5 =>  println!("여기에서 코드를 수정하고 실행할 수 있습니다!"),
            6 =>  println!("Ten kod można edytować oraz uruchomić!"),
            7 =>  println!("Este código é editável e executável!"),
            8 =>  println!("Этот код можно отредактировать и запустить!"),
            9 =>  println!("Bạn có thể edit và run code trực tiếp!"),
            10 => println!("这段代码是可以编辑并且能够运行的!"),
            11 => println!("Dieser Code kann bearbeitet und ausgeführt werden!"),
            12 => println!("Den här koden kan redigeras och köras!"),
            13 => println!("Tento kód můžete upravit a spustit"),
            14 => println!("این کد قابلیت ویرایش و اجرا دارد!"),
            15 => println!("โค้ดนี้สามารถแก้ไขได้และรันได้"),
            _ =>  {},
        }
    }
}

So, I started out with minor re-learning (how to do "classes" in Rust, and how to inherit), porting my existing engine source over to Rust.

class Object {
    doSth() { cout >> "Do Something!" >> endl; }
}

class StaticMesh: public Object {
    doMeshyThing() { cout >> "Do Meshy Thing!" >> endl; }
}

became

trait TObject {
    fn doSth();
}

trait TStaticMesh {
    fn doMeshyThing();
}

pub struct Object;
pub struct StaticMesh;

impl TObject for Object {
    fn doSth(&self) { println!("Do Something!"); }
}

impl TObject for StaticMesh {
    fn doSth(&self) { println!("Do Something!"); }
}

impl TStaticMesh for StaticMesh {
    fn doMeshyThing(&self) { println!("Do Meshy Thing!"); }
}

Whoops. Just one moment there. If I want to have something inherit-y, I have to re-implement everything? Ouf. Well, we can always write a function and call that from each method, right?

trait TObject {
    fn doSth(&self);
}

trait TStaticMesh {
    fn doMeshyThing(&self);
}

pub struct Object;
pub struct StaticMesh;

fn doSth<T: TObject>(obj: &T) { println!("Do Something!"); }
fn doMeshyThing<T: TStaticMesh>(obj: &T) { println!("Do Meshy Thing!"); }

impl TObject for Object {
    fn doSth(&self) { doSth(self); }
}

impl TObject for StaticMesh {
    fn doSth(&self) { doSth(self); }
}

impl TStaticMesh for StaticMesh {
    fn doMeshyThing(&self) { doMeshyThing(self); }
}

Oh no, looks worse than before, and how am I ever going to decouple modules like that?

...and Failed

After porting enough code to actually load assets, render Sponza and navigate around using the keyboard, I gave up.

I had packed many hours into porting to Rust. I wrote a renderer, using Vulkan (which was also new to me, by the way). I wrote an asset loader. I wrote an input library. I wrote bad and dirty code. And I became less and less motivated.

The compiler constantly told me that I cannot borrow, or that my a variable doesn't live long enough, or that a value moved before being used elsewhere. Finding out what the compiler meant wasn't easy. Sometimes, there was a message giving me a hint, and most of the time, following said hint worked out great! Other times, following the hint led to more errors, more hints, until I created a hint-loop. Baaaad.

To be honest, the book did make resolving errors sound easy, however, the docs were very generic and not helpful at all. By the end, the clean looking C++ library had turned into a monster of Rust hacks and workarounds, which I all put up with as "learning experience" and "technical debt for later".

The only thing I swore not to touch as a beginner was the "unsafe" keyword, which disables a lot of Rust's safe-guards. I was still not knowledgeable enough to do so in a safe way, so I did not want to ruin all of Rust's benefits just by writing unsafe code.

I know, others would ask me if I am stupid, learning a new language by learning a new technology in a project type I wasn't familiar with. I'd say, they are right! That was stupid. However, I learned a lot. I learned the hard way, that the compiler is the worst nit-picker. I learned that Rust does not work like C++, JS, Delphi, or any other language I had encountered before. And that's what's great about it. Only then, after all that hardship, the learning journey could begin.

Twisting My Head Around New Principles

There are a lot of great things written in the book about the principles and inner workings of Rust, but if there is anything to take away from what I just wrote above, it is that they all seem easier than they are. So, here's what I did: I did go through each Rust pattern in the book.

For each and every single one I created at least one program which focused on that one pattern. These little programs ranged from something non-sensical like very complicated hello-world outputs, to something small and fun, like an address book with a linked list.

Also, I had lot of success using CodeWars, working my way up, solving puzzles, which usually would be super easy for me, but posed a new challenge using Rust.

Well, to be fair, I sometimes did challenges in JS and C++ first, then went for Rust. Maybe I started to chicken out there 😅. However, getting to know all the stuff, which make Rust hard great, paid off!

Monads

The first thing, which stood out to me as important, were the so-called Monads. At least the simplest ones, which are used all over the Rust APIs. I am talking about Option and Result. As for Option, the docs read:

Type Option represents an optional value: every Option is either Some and contains a value, or None, and does not.

What does that mean? To make it simple, think of any situation, in which you might have a value, or not. For example, when initializing a program, you might want to read a config file, but must first create the structure holding the data, in order to actually read the config.

Question: What do you fill the struct's fields with? Traditionally, null, undefined, 0, "", etc. Those are values which you defined as initial values. Let's say, an error happens: the config does not exist, however, the program continues execution (missing error handling). The program would use nonsense values, and it might be hard to debug, as the problem is not transparent.

However, what if we could tell the program that there is indeed no value, not even null, as a placeholder? That's where Option comes in.

Option is a generic container, which can hold either a value, or nothing. In Rust, it is implemented using an enum, which only exists at compile-time, but disappears at runtime, because it's just a concept, no actual machine instructions or data. Zero overhead, yay!

pub enum Option<T> {
    None,
    Some(T),
}

So, we can declare a variable, which contains an enum value and reserves space for a value, but also knows by contract if one actually exists.

An Option-type variable can be initialized to None, and later filled via Some. It can, at any time, be queried, if it contains a value or not.

In addition, if there is no value at run-time, the program will panic if the error is not handled correctly because that's an error in the logic. No undefined behavior possible.

fn main() {
    let mut i: Option<i32> = None;

    if i.is_none() { i = Some(0); }
    println!("i contains {}!", i.unwrap());
    // Prints:
    //   i contains 0!
}

Option eliminates null, which is regarded as one of the greatest regrets in the history of programming by a lot of developers. What about Result, then?

Actually, Result is a new take at error management. First of all, think of a function, which should find an index, but also should communicate a not-found result. JavaScript implements that by returning -1, which is an out-of-bounds index. It is not communicated as an error, but as a regular result, and has to be handled by the API user.

Now, think of a function, which might fail and has to propagate an error. In many languages, the function would simply throw. Throw, however, often causes undesired side effects (like gathering a stack trace), which are not very performant, and leave it up to the implementer to document the error throw, in addition to the user actually catching the throw. If a throw is never caught, the application exits. Most of the time, though, that's because of a programmer forgetting about handling a throw or looking up if a throw can happen at all.

As you can see, these kinds of error handling are inconsistent, easy to miss, and will lead to a fatal error in the application. Result to the rescue!

Result<T, E> is the type used for returning and propagating errors. It is an enum with the variants, Ok(T), representing success and containing a value, and Err(E), representing error and containing an error value.

Just like Option, Result is an enum-container. It may contain either a result or an error:

pub enum Result<T, E> {
    Ok(T),
    Err(E),
}

Any function, which has to communicate an error-case, can simply return a Result, and any API user will have to consciously handle it - without looking up the docs.

fn divide(a: i32, b: i32) -> Result<f32, &'static str> {
    if b == 0 { return Err("Cannot divide by 0!"); }
    Ok(a as f32 / b as f32)
}

fn main() {
    println!("4/2: {:?}", divide(4, 2));
    println!("17/0: {:?}", divide(17, 0));
    // Prints:
    //   4/2: Ok(2.0)
    //   17/0: Err("Cannot divide by 0!")
}

Rust consolidates error management and introduces the error handling by contract, visible directly in the function signature. That's awesome!

Traits and Declaration-based Composition

The next thing which was painfully obvious to me, was that I had absolutely no friggin' clue as to how to use traits and structs. Data containers with associated methods. The one thing I was so very used to. The one thing which was clear to me, though, was that Traits were merely interface declarations. The stuff one would write into header files in C++, or at the top of a Unit file in Object-Pascal.

They are pretty comparable to Interfaces in TypeScript. Using them for Inheritance was like putting car wheels on a boat because I am used to driving a vehicle with wheels. So, Traits declare only a certain contract, however it is up to an implementation to deliver the definition.

When I learned about inheritance, I learned about it using game dev. You create a base class, then add features in another class, which inherits from the first. For instance, you create a Monster class, which implements all the monster stuff, and then you create a Werewolf class, which only adds the werewolf specifics on top of the Monster class.

Inheritance, is limited, and in most languages runs into some kind of diamond-inheritance problem, or functionality-cherry-picking madness. According to many clever minds, like the Gang of Four, composition is a lot better and should always be preferred. Back then, I knew composition. In JS, you'd go:

const A = {
    foo: () => {},
};

const B = {
    bar: () => {},
};

const C = {
    foo: A.foo.bind(C),
    bar: B.bar.bind(C),
};

Well... I thought I knew composition. Actually, the above example is pretty close to something Mixin-like: remixing existing objects to something new. Definition-based composition, if you like. That's not possible in Rust, though, and with good reason.

Imagine the following: You are tasked to change A.foo() in an unknown code-base and implement a new check for something. That's what you do. You test your change, everything looks good, but suddenly, tests all over the place start to fail, but they have nothing to do with the object you just changed. Whooops. Have fun untangling that mess.

That's why composition in Rust can only happen with declarations, which do not contain code, only the contract. The upside is: the code for every definition is separate. No surprises. Everything's boring.

The downside is, obviously, every implementation has to re-define the actual code. Which is not too bad, considering we still can use functions for re-use, but can get annoying for once big hierarchy-chains. To put it in a nutshell: Traits declare a contract. Contracts can be mixed and matched. All Traits, which are implemented on a struct, have to be defined by the struct.

Let's Code Something

Having the principles down, I started a new try at coding something. I did learn from my previous attempt to go for something I am familiar with.

What some of you might know is that I have been working on writing a minimal-config web server, which automates security away and can use plugins for all kinds of stuff (like using Handlebars instead of just serving static files).

After all, security is important for me and much too hard for both, users and developers, today. I want an easy solution, and I have been creating something_productive™ (which actually does run in production!) in JavaScript on NodeJS before. I was several years into the project, so I would say I am fairly familiar with what I want and need.

Marco and the Module System of Terror

First things first. I started creating a module structure for the thing I wanted to have in the beginning.

  • A clean shell,
  • A config handler, and
  • A web server.

No threading, yet, I would do that later on. Rust can have modules (like namespaces in C++) automatically using the file system. Since I wasn't sure how many files I'd need per module for better separation inside, I created folders.

/src/
  |- shell/
       |- mod.rs
  |- config/
       |- mod.rs
  |- server/
       |- mod.rs
  |- main.rs

The system is simple: Start in the main crate and mod + use anything to pull it in. Mod declares that there is a module to pull in - and mod is only needed once per library or application. Use can make the usage of nested imports or structs inside modules fairly easy, for example:

mod shell;

use shell::Shell;
use shell::ShellHelper;

fn main() {
    let s = Shell::new(ShellHelper{});

    // ...
}

...in theory. What tripped me up, though, was

  • #include... C++ was like a boomerang, coming back and hitting me whenever I expected it the least 😕
  • Do I have to use mod in each module, too?
  • What is the scope of a module? At some point, I thought that all modules had their scope in /src...
  • What is ::, what is super:: and what is self::?

Fortunately, the compiler did not take long to compile an empty project (just a few seconds(!!!) per run). For your information, I did have to run cargo build. There was no cargo check, yet.

The error messages were not very helpful, and the book was confusing, for me at least. Today, the situation is a lot better, and I found out that the rule of thumb is:

  • Only use extern crate and mod once in your project per module. Once. Ideally in your main file.
  • A module is scoped to its own folder, but going a scope up is easy using super::
  • Forget about ::, super:: is one scope up and self:: is this scope, so it's useless, too, for the most part.

I think, it was mostly on me, however after wrapping my head around the mod system, things started to click in place and developing became a lot smoother - finally knowing how to do the decoupling part.

Can't Steal from the Borrow Checker

With modules out of the way, I felt unstoppable. So I started with the simple stuff: initializing my modules. Have you ever thought about the following:

What happens if you read from a variable, but at the same time write to it from a different thread?

While in my single-threaded application, something like that mostly does not matter, Rust does not even know about the single-threadedness and hence unleashes all the power of brutal nit-pickyness. Oh dear. See, if I work with a value, Rust "borrows" it for that period of time. No one else is allowed to touch it during that time, only read it (or, if I want to write to it, no one is allowed to read while I borrow it mutably).

fn main() {
    let x = 0;
    let _y = &x;

    // will not compile
    // x = 1;

    println!("{}", x);
}
fn main() {
    let mut x = 0;

    {
        let _y = &x;
    }// _y is dropped here, because all variables are dropped at the end of their scope

    // will compile
    x = 1;

    println!("{}", x);
}

So, in the config module, it happened that I stored configs in one vec (it's the short for Vector, which is an array with a dynamic size), but wanted to also have them in a second one. Plus write to them, of course. Silly me. There came the Borrow Checker with its big No-Borrows-Hammer.

fn main() {
  let configs: Vec<String> = vec![
    String::from("foo"),
    String::from("bar"),
  ];
  let mut baz: Vec<String> = Vec::new();

  baz.push(*configs.get(1).unwrap());
}

cannot move out of borrowed content

The problem is, that I take a reference out of the first array (borrow it), but then de-reference the value, so that it then would also be owned by the second array. That's theft!

From Rust's perspective, though, even more, problems might arise, like when clearing the second array, the first array would contain invalid data. Rust is safe. Always. So that's a no-go.

From my perspective as a developer, I was shocked that I created such a simple memory bug so easily in my code without even noticing it. However, what are the alternatives?

  • Store a reference. However, above limits apply
  • Change the ownership, but I wanted to have the value in both arrays
  • Copy the value, which is slow and also means that I'd have to keep both arrays in sync somehow, which is bothersome and slow
  • Use shared access, implemented via reference-counting

By the way, all of the options didn't sound too good at the time. Sane programming standards are hard. For the lack of a better architectural solution, I abandoned the module and hoped to find_a_solution_later™.

When You Need to Outlive 'a Lifetime

Did you ever think there's a monster hiding under your bed? Well, in case of Rust and me being a newbie, there were two. The borrow checker had something to say about my code every single time I tried to compile anything. Every single line.

However, from time to time, another error message surfaced, making me grumble, in my world of pain and learning and becoming a Rust developer. 😅 Plus, in my humble opinion, it is the other one which haunts developers for a long time. The simplest instance is when a value is dropped while still referenced (for example borrowed).

fn give_ref() -> &i32 {
    let i = 9999; // `i` is owned by this function. It will be cleaned up at the end of it

    &i // this line, however, returns a reference to `i`
}// even though `i` is cleaned up here

Above, you can see that i is cleaned up, however a reference to it was returned from the function. So, the caller of the function would receive a reference to invalid memory.

Rust calls this principle "Lifetimes". A Lifetime defines how long a value lives before it is cleaned up (for example goes out of scope).

Imagine the following: You can always ask your friend a question. Something simple, like "What are you", and they will tell you that they are a "human" - which is true (and which you cannot change, except if you can do some magic). However, once they die, if you ask them this very same question, the only answer will be the silence (assuming you don't dabble in witchcraft). Silence is not the right answer, though. It's invalid. While we can process it, a computer can't. So a computer will have to make sure that questions can only be asked while something is alive. Well, the computer has the role of a god in this situation, because it reigns over the life and death of variables - you get the point.

Just like with borrowed content, there are options:

  • Pass by value, so that the Lifetime is prolonged to the parent scope
  • Use reference counting, which lets a value live while something is using it
  • Use a 'static Lifetime, meaning it exists while the program exists

They have advantages and disadvantages, depending on the situation, and it is in the hands of the programmer to find out a suitable way to solve the problem at hand.

My Experience with Rust in a Nutshell

All in all, I was able to get the module system under my control, however I had to learn to reckon with the Borrow Checker and Lifetimes.

I, again, concluded the project with "need more practice". While I don't think that Rust, by itself, is hard to learn, I'd say that it is actually sticking to safe logic, which is rather difficult.

Too many principles seem overly abstracted and easy in other languages, while in reality they are not, and might not even be safe. Rust is transparent about safe memory handling. I will stop anyone from doing dumb things. All the problems above do make sense and look neat in the examples, but I actually dripped more often over them than I want to admit.

Safely handling memory also means thinking about many things in a different way. Sometimes, I wonder, if we didn't start over-engineering hardware by writing in abstract languages, however I also think that we would have never gotten this far without abstracting away hardware, because hardware is difficult and in most applications not something anyone wants to focus on.

All the above principles do sound advanced to me, when compared to what older languages have to offer, though. So, I, at least, hoped that people more clever than I could use Rust to do awesome things and offer a better future for computing in general.

One Night in TypeScript

After spending so much time with Rust, I needed a break. Let everything sink in a bit. Get some instant gratification and motivate myself. So, I did what I am good at, and what delivers. Web development. Actually, I did some refactoring on my web server project, throwing away old code, setting up new, decoupled modules in their own repositories, re-usable and all that stuff.

Did you know that there was no shell module available on npm? Well, I added one and while writing and writing, I made one astonishing discovery. I missed Option and Result. And what about Trait? Certainly, there was Interface, but I had never read about it being used in a declaration-based composition-y way before. JS is more about Mixins, after all. Did no one care?

...and the World's Your Rust Playground

So, I started experimenting. Certainly, there were monadic modules on npm, but they were either abandoned, or based on a different API (than Rust). I decided to give learning Rust things even another try. Just this time in an environment I knew well. I knew the languages, and the project. Just one new concept.

Surprise! Even more mind-bending. Painstakingly, I had to find out, that there was more to learn about the Monads of Rust and their implementation. Did I even read the docs back then? It took me four major versions until I finally got the Result API and implementation right. FOUR. I didn't need the Options as much, though, so I kept them simple and mostly defect. To this day, I wouldn't recommend them to anyone 😆

That's when I thought to myself: What about Traits and declaration-based composition? - and I fleshed out an idea how to use them. It lessens my desires for simple(r) Rust a bit while giving me the air of a known language.

Keeping strictly to what Rust toughed me started to make sense in other languages! I felt like exploring Rust all over again, however from a different perspective this time. What if Rust is only the means to teach good programming, by forcing good style on its users? I took in too much at once in the beginning, but slowly building up knowledge? I can do that.

It's a Treasure!

What I kept quiet about is the game project I have been working on. You know, a game dev would not really want to write an engine in the first place, right?

I felt like I couldn't do much, but gave game dev a try with the help of a framework. In the beginning, for 3D, there were only two native options:

  1. Piston, which already had some level of maturity and some projects to show, and
  2. Amethyst, a lot more incomplete, however highly experimental, and incidentally implementing everything I always dreamed about having in my engine ❤

That's right, I chose Amethyst. I took an example and started out with a simple game of Pong. At the same time, they implemented their own Pong game, so I could cross-check, which helped me a lot. Then Snake. Then SpaceInvaders. Then a 3D sandbox. And then, Amethyst changed. Of course, the project was still in very early development (it still is, but a lot more mature now).

As a result, and because of all the changes, I threw away everything. And started all over. Again, just when I caught up, Amethyst pushed new changes, which I wanted to have... and ones which I didn't want. Again, I threw out all my code and started from the beginning.

Each iteration became better and better, and to my great joy, had the feeling of being better than the last. I read the engine source, tried to add value to it, and improved my Rust. After all, becoming good at something is all about practicing hard. Amethyst implements some fine code by some clever people, so I made it a habit to at least read some of the upcoming pull-requests, and write more game code, just to throw it away again 😂.

By now, I have to say that I love writing Rust. It became a lot easier to write safe code, without the compiler throwing up on every single line I write. I credit a big part to Amethyst. It has become quite literally a treasure for training me. However, there's even more. Because of writing more and more game code, I had to look up way more Rust APIs and how to use them.

So, I made contact with what I think is the world's best documentation. It was a bit hard getting used to reading it, but by now, Rust documentation, which is auto-generated and all looks and feels the same for all crates in the repository, is easy to navigate and understand. It usually contains a great deal of information - especially the standard ones.

Not even the Mozilla developer network can get there - with its only advantage of containing more examples and informational text. The Rust documentation and tools are pure gold.

For me, Rust is slowly, but steadily, turning from the ugly duckling to a treasure trove. A treasure I can pick up and take with me to whichever other language or occasion I want, and then use it to the best of my abilities, creating surplus value.

Conclusion

Coming from an entirely different mindset, branded into my brain by other languages and paradigms, I had a rough start. Having a little prejudice didn't help, either.

However, through lots of failures, repeated attempts, and especially taking a step back, I was able to get a grip on one of the world's most loved tools. For me, it's not only a language, but a big box of many different things I can use. It's a bag full of knowledge and experience by very clever people.

Built around it is a supportive community with people who tend to give thought about what they do in their code, but also what is best for the ecosystem as a whole and how to onboard different kinds of developers.

Rust is an ideal environment to learn good style and social competencies at scale. It teaches how to become a super-developer, loving all parts of the development cycle. I am still learning, and I will continue to do so, because Rust got me hooked.

Please share this tutorial with any of your friends who are just getting started with Rust, and let me know how you liked it!

Marco Alka

Software Engineer, mainly working as FullStack and DevOps developer at Robert Bosch GmbH. As a hobby, I also do Game Dev, Embedded IoT tinkering & I mentor @ https://mentorcruise.com/mentor/MarcoAlka

Write your comment…

7 comments

Followed from _this_week_inrust and I liked your post. Some sections were difficult to parse in English, but overall it's inspiring and motivating for a new Rustacean like me ( < 1 month learning)!

One suggestion is to change the divide function to not introduce the concept of 'static which seems kind of distracting for the beginner audience. Here is an alternative:

fn divide(a: i32, b: i32) -> Result<f32, String> {
    if b == 0 { return Err(String::from("Cannot divide by 0!")); }
    Ok(a as f32 / b as f32)
}

About game development-- I also dabbled in game dev in the past, using Unity3d. You are right to not attempt developing a game engine from scratch. Too much work, you will never finish a game! Just leverage an existing game engine, and try to make a great game, then another and another!

That said, even though it is at version 0.x.x , amethyst looks totally awesome!

Reply to this…

Share your programming knowledge and learn from the best developers on Hashnode

Get started

Thanks for this great article. It sounds similar to my experience with Rust: I picked it up for a toy project. It was a blast at first, but then, after few days and couple hundreds lines of code, there came lifetime errors I really could not resolve. I tried and tried but finally gave up and put Rust on a shelf labelled "maybe try out again later".

I never did, although for some time I'm starting to think about it. I think you have just encouraged me to do so, as your story is similar, but you managed to fully ovecome the difficulties. Thanks again!

Reply to this…

Just to be pedantic, you can delegate on implementations rather than interfaces, right? Just delegate to a struct (which also have methods).


I had some trouble getting used to the borrow checker too. But seeing it is the one big thing people warn about with Rust, it went faster than I thought.

I struggled more with things like object safety and with orphan rules. For some reason they're hard for me to get used to - it just doesn't come up in Java or Python...


The Rust documentation and tools are pure gold.

That doesn't include the IDE support as a tool though :-) I'd be a lot more productive if Rust had the IDE support that Java has. (I use CLion which has okay Rust support, but it's going to take time catching up with the long-established languages).


I also have the problem of wanting to write Rust-style code in other languages.

I didn't like it before either, but now I've really developed an aversion to Java objects being references from multiple places. If there'd just be one owner like in Rust then I could assume the object isn't changed.

Smart people had said before that mutable state is hard to reason about, and I believed it. But I only really understood the benefit when the compiler enforced it.

At first the borrow checker seemed like bookkeeping to prevent GC. But that seriously| underestimates it (and your article rightly mentioned threading protection prominently).

But the problem with threading is that your state is changed between lines. That problem still happens to some extent in a huge single-threaded codebase. Sure I set an object field here, and then use it later, but I'd have to understand all the lines in between, recursively, to know my state didn't change.

Reply to this…

Hey, look at this programming language! It's so cool!!!", however when I saw the syntax, my head started spinning and I could only think: "Well, yeah..... no. What an ugly syntax. What a roundabout, complicated way to get things done. I can do the same with modern C++ and have more control at the same time."

Exactly my thoughts now, with just one week being with it. Hope it's gonna change soon, as I am realizing how fast and efficient it is coming from JS(Node) background.

Thanks for sharing such a wonderful article. :)

Reply to this…

Marco Alka nice article. I too learned Rust recently (last year) and I definitely appreciate its strengths. I haven't used it in a bit though for anything useful because I found that most of the times I am reaching for C, it's because I want to do unsafe things... So reaching for Rust instead just became overburdening for most tasks that I would use C for. I also found some oddities within Rust such as no direct built-in access to raw sockets??? And it's called a systems level language??? Strange.

Also, I found byte-manipulation to be overly cumbersome and involve a bunch of crates... I would expect strong byte manipulation out-of-the-box in a systems-level language.

I would want Rust over C for developing production software because at that point, it's a bigger endeavor anyway, but for quick tools and hacks, I still prefer C for many things like networking and working with bytes. However, I do reach for Rust for threading and working with strings over C for sure.

We'll see how Rust does in the future.

Show all replies

Hi Todd, thank you for your comment! Rust is a safe language, so it might be a bad match for pen-testing and required unsafe memory operations. It's not a fits-all solution 😉

I also found some oddities within Rust such as no direct built-in access to raw sockets???

I never really needed raw sockets - or byte manipulation, for that matter - but my guess is that they did not add it to the stdlib, because they want to keep it small. They removed many things and you will have to pull in the crates you need. However, I do have to wonder, what exactly are you missing byte-operation wise? Afaik, Rust has all the operators and types in place...

I agree with you, though, that Rust has its strength in modern application design, because of its thread safety and Strings, as well as many modern programming paradigms and know-how built-in. Rust might become quite important on our trek to using more and more cores in parallel.

Reply to this…