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?
- Taking the Leap and Becoming a Rust developer
- Twisting My Head Around New principles
- Let's Code Something
- One Night in TypeScript
- It's a Treasure!
- Conclusion
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: everyOption
is eitherSome
and contains a value, orNone
, 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 Interface
s 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, Trait
s 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 eachmodule
, too? - What is the scope of a module? At some point, I thought that all modules had their scope in
/src
... - What is
::
, what issuper::
and what isself::
?
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
andmod
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 andself::
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 Trait
s 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:
- Piston, which already had some level of maturity and some projects to show, and
- 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!