Evaluating the Rust Programming Language: I don't like Rust so I'm learning it

in programming •  6 years ago 

Screenshot_2018-11-30 Rust - index.png

I've decided to take a deeper look at Rust by working through the official book and writing a few small applications.

My first thought when I saw Rust a few years ago was "This is horrifying, a huge step back for programming." Mostly because the Rust code I've seen looks horrifyingly complex, like bad C++ code (where bad code is defined as unreadable), while other modern systems programming languages like Nim, Red, Go, Swift and Vala, have shown that it doesn't have to be this way.

However, some people are horrified about Lisp code, which I actually find visually pleasing, about Haskell and Elm, which I got used to, and you can only honestly say something is bad if you truly understand it, so I decided to actually learn Rust and see what's what :)

Why Rust?

Rust is growing ever more popular, it won "Most loved language" three times in a row in Stack Overflow's 2018 Annual Developer Survey, I see more and more projects switch over to it from Go, and if the question is "Am I wrong, or is everyone else wrong", the answer is probably I'm wrong :) So it's time to really figure out if my first impression was just prejudice and I should start using Rust myself, or if my gut feeling was right and other systems programming languages like Red are better suited for most tasks and Rust is just useful for a very small set of very specific things where those other languages fall short.

Another reason is that I started to look into Reason, Facebook's more JavaScripty syntax for OCaml, and ReasonReact, which is the ReactJS binding for Reason and where Facebook tries out new features that ReactJS might adopt in the future. I use React a lot, but with ClojureScript and the Reagent React wrapper. What does all of this have to do with Rust? While Rust isn't officially part of the ML family of programming languages like OCaml, it is heavily inspired by it, the first version of Rust was written in OCaml and then later rewritten in Rust itself, and Rust offers the same defining features like pattern matching, which I've come to really like from trying out Haskell, Elm and now Reason.

My programming background

I've been programming since I was 8, started with BASIC, I'm 35 now so it's been a while :) I've done both backend and frontend web development and both native and Electron/node-webkit desktop application development, have played around with native and web based mobile development but I haven't published any mobile apps yet. So I've got quite a wide range of experience. Lately, over the last few years, I've been mostly doing frontend web development but I've started going back to desktop application development a year or so ago in addition to web development.

I mostly program in languages without static types, I'm not convinced that static typing is "the one true way" of programming, but the industry seems to be moving in this direction and I am always open to the idea of being wrong, so I'll learn more about the different statically typed languages out there so that I can maybe change my mind with better information, or at least can honestly say I've deeply looked at all options and think my position is correct.

Just so you know where I'm coming from before reading my observations on Rust, I used Swift for one project and really liked it, I used Go for one project and found it very fun to learn and use but ultimately decided there are better options out there, I'm using Vala right now in one project, and my personal favorite in the general purpose systems programming language category is the Red programming language which I'm writing multiple applications with and also a large tutorial / online book and screencast series called Learning Red.

I know about 60 programming languages to varying degrees, but listing all of them here would be crazy and these four languages I mentioned make the most sense in a comparison to Rust out of the set of languages I know. I do of course also know C and C++ (although I haven't used them in many years), but I think it's better to compare Rust to other modern languages. I've got fairly limited experience with systems programming overall, I tend to use dynamic languages more often, but I've become a bit sick of big and bloated applications, so writing code at a lower level to create smaller and faster applications has become a priority for me.

My setup

I'm using Emacs right now as my Rust IDE, or rather Spacemacs which comes with lots of programming languages pre-configured, including Rust. Just had to install the company package manually to get auto-complete working, not sure why that didn't came out of the box. But it's working great so far, I can click on errors to see more information, can use simple key combinations to build or run Rust projects with cargo, it's quite simple to use. I've also installed the plugins for Visual Studio Code, Atom and Sublime Text 3, so I'll be trying those in the future as well to see which one offers the best overall Rust development experience.

Screenshot from 2018-11-30 13.54.02.png

Evaluating the Rust Programming Language

This is going to be a series of posts that I write over time as I learn more about Rust. Right now, I'm at chapter 8 of the Rust book and I've played around a bit with Gtk-rs, the GTK bindings for Rust, since GUI (and web) development has always been my main focus.

I'm going to start this series of posts off with a list of pros and cons about Rust that I see at my current point of understanding. My opinions here might change as I learn more of course and you'll be able to see if that's the case starting with part 2 of the series. I might explain something wrong because I don't remember or understand something correctly, so if you do notice a mistake please don't hesitate to point it out, it'll help me learn so it's much appreciated.

I'm not sure if this series of posts about Rust is going to be interesting for anyone to read, but that's okay, writing these things down helps me memorize what I've learned about Rust so far and deepen my understanding of it, since trying to formulate something you just learned in your own words shows you exactly what parts you are unsure about or forgot, so that's already worth a lot and if someone else reads this and enjoys it or shows me where I understood something wrong, that's an added bonus :)

In future parts of this series, I'll also show off some neat libraries and applications written in Rust that I stumble upon, so it might become more interesting at that point.

The pros and cons of Rust

Pros

Documentation

Rust comes with it's own programming book to teach you the language, called "The Rust Programming Language". It's available as a printed book which is now at the second edition, online for free on the Rust website, and even comes locally installed on your computer with the rustup tool, just enter rustup doc in a terminal to open the documentation including the Rust book in your web browser, offline. The cool thing about the book is that it's very approachable. It's not written for systems programming experts or academics, but for everyone. Since I don't have a formal computer science education but am a self taught programmer, I much appreciate texts that don't require you to know the confusing jargon you learn at university.

It doesn't just stop at the book though, the Rust standard library documentation is pretty good as well, easy to understand with examples and thorough explanations. And if you look at the beta version of the new Rust website https://beta.rust-lang.org/, which has just been released yesterday, it has been made a lot more approachable as well (although the design is a matter of taste) and prominently features at the very top the line: "The programming language that empowers everyone to become a systems programmer". The old site said instead "Rust is a systems programming language that runs blazingly fast, prevents segfaults, and guarantees thread safety" at the top. Which means absolutely nothing to someone who is new to systems programming.

Systems programming languages usually are anything but approachable, so Rust doing something different here is really cool.

Immutability

Rust is immutable first, but allows you to opt out of it where you need to. I really got to like the concept of immutability while using Clojure & ClojureScript, it's a great fit for functional programming. But the immutability in Rust has an overhead, so if you do need more speed in one area of your code, for example when modifying large data structures, you can opt out of the immutability and use mutable variables.

Pattern Matching

Rust is very close to the ML family of languages, SML, OCaml, ReasonML, F#. Or the languages heavily influenced by it like Haskell and Elm. You've probably heard of at least one of those. One of the defining features of these languages is pattern matching, and Rust is no exception here.

In Rust, you pattern match on enums, so Rust's enums are basically enums on steroids. The different states the enum can have are called variants, and a variant can have data attached to it. And just like you can define methods on a struct, you can define methods on an enum. So in effect, an enum is a more powerful struct that lets you use pattern matching to handle the different variants of that enum.

If you've ever used Elm, Re-Frame or Redux for modern web development this will be familiar to you, since it's the basis for what Elm calls "The Elm Architecture" which is also used by Re-Frame in ClojureScript and Redux in JavaScript + React.

The typical first example you see for "The Elm Architecture" is a simple counter, just a label that shows the current count and + and - buttons that let you increase and decrease the count, maybe a reset button if the example is really fancy :)

I'll show a very simplified version of how you could do this in Rust using the language features it provides.

You first need a model which holds all of the application state, so that there is only one place in your app where state is and gets modified. In the case of the counter the model is just a struct with a single integer value representing the current count:

struct Model {
    count: u32,
}

let mut state = Model {count: 0};

Then you need messages/actions that you can send from anywhere in your application, and the function that processes these messages then modifies the model. This is a two part step, we first need to create a Message type which lists all the possible messages to update our model. We're going to use an enum for this. Increase, Decrease and Reset don't have data attached here, but Add has a u32 integer value, just to show how that works:

enum Message {
    Increase,
    Decrease,
    Reset,
    Add(u32),
}

Now we create a dispatch function which we can use to send messages that update our state, using pattern matching on the message:

fn dispatch(state: &mut Model, msg: Message) {
   match msg {
       Message::Increase => state.count += 1,
       Message::Decrease => state.count -= 1,
       Message::Reset => state.count = 0,
       Message::Add(num) => state.count += num,
   }
}

Sidenote: As I said, you can also create methods on enums. Since the dispatch function is so closely related to the Message enum it would make sense here. All you need to do that is to wrap the function inside the curly braces of impl Message {} and then you call the method with Message::dispatch instead. I just didn't do that here to keep the code as simple as possible.

Now a message can be sent like this, with a mutable reference of our state (Model) and the Message variant we want to send, in this case Add:

dispatch(&mut state, Message::Add(2));

Here is the complete code:

struct Model {
    count: u32,
}

enum Message {
    Increase,
    Decrease,
    Reset,
    Add(u32),
}

fn dispatch(state: &mut Model, msg: Message) {
    match msg {
        Message::Increase => state.count += 1,
        Message::Decrease => state.count -= 1,
        Message::Reset => state.count = 0,
        Message::Add(num) => state.count += num,
    }
}

fn main() {
    let mut state = Model {count: 0};

    dispatch(&mut state, Message::Add(2));

    println!("The count is now {}", state.count);
}

This will print The count is now 2.

Interestingly, there is actually a GTK wrapper for Rust that makes use of the "The Elm Architecture" design pattern, called relm. It's an alpha and doesn't make use of Rust's immutability yet for the Model, but it's very interesting. I hope it doesn't get abandoned before it's finished, which is sadly something that happens often with these kinds of projects.

Error messages

Error messages in programming languages tend to be rather cryptic and not very helpful for beginners. It's great to see that Rust is an exception here. Just like the official Rust book which is easy enough to understand for beginners, the error messages are helpful and often suggest how you can fix a bug in addition to just displaying the error.

Screenshot from 2018-12-01 02.13.46.png

The errors also often link to more in-depth documentation for the error, so if you still don't understand what went wrong, you can look it up. In Emacs for example, you can just click on the error number and it will open the documentation for it.

Screenshot from 2018-12-01 02.14.36.png

Error handling

Forced error handling is something Go has become (in)famous for. Go makes you handle every error and people find that tedious. You should handle all errors, but sometimes, to be more productive (or lazy), programmers leave out the error handling for situations where they assume an error will never happen. The result are applications that aren't as safe as they could be, but work fine most of the time and were written in a much shorter amount of time which can be preferable. Not every programmer or company has the sheer limitless resources of Google.

Rust does something intriguing here. First of all there are two kinds of errors, recoverable and unrecoverable errors. Trying to access an out of bounds element of an array for example is an unrecoverable error (which is great because it means one common security hole is closed in Rust automatically), so the panic!("Some error message") macro is called, which unwinds the stack (can be turned off) and quits the application with an error message logged to the console. Recoverable errors are things like trying to open a file that doesn't exist, you might want to create the file instead of just crashing the app.

You don't have to handle every error. But you get a compiler warning if you don't. So it's optional, but you still get the benefit of knowing if you forgot to handle an error. And Rust also offers shortcuts to handle recoverable errors, allowing you to decide if and how much time you want to spend handling an error. Recoverable errors are represented in Rust as an enum Result<T, E>.

You can either call expect on the enum, which will call the panic! macro to make the app quit with a message you provide:

let some_value = some_function_that_can_fail()
    .expect("A message that gets printed to the console");

Or you can call unwrap on it, which will call the ** panic!** macro and display the standard error message associated with that error:

let some_Value = some_function_that_can_fail().unwrap();

Or you can actually handle the error gracefully so that your application doesn't crash, by using pattern matching on the enum:

let some_value = match some_function_that_can_fail() {
    Ok(value) => value,
    Err(error) => {
        println!("Oops, there was an error: {:?}", error);
        // do some cleanup here to recover from the error
    }
}

Memory Management

Rust doesn't have a garbage collector, but does something similar to systems programming languages like Objective-C or Vala so that memory is still managed automatically for you most of the time, you don't need to explicitly allocate and free memory. Vala for example uses ownership and reference counting and Rust uses ownership and borrowing.

Scalar values are put on the stack in Rust, everything else is allocated on the heap with the pointer to the memory location stored on the stack as you'd expect.

fn main() {
    let myInt = 42;
    let myLiteralString = "Hello";
    let myHeapString = String::from("Hello");
}

In this example, myInt and myLiteralString are stored on the stack, they are popped off the stack once they leave the current scope after the closing }. myHeapString is a pointer on the stack which points to a String value allocated on the heap. String::from(aLiteralString) creates a new String from a literal string. When myHeapString goes out of scope after the closing }, it's drop method is called automatically which frees the memory that was allocated on the heap.

The myHeapString variable owns the String. But that is only half of the ownership concept. The other half is this:

fn main() {
    let s1 = String::from("Hello");
    let s2 = s1;
    println!("The value of s1 is: {}", s1);
}

This should print The value of s1 is: Hello, but instead it produces a compile error. By setting s2 to s1, the ownership of the String was moved from s1 to s2 and s1 is no longer allowed to be referenced. This helps with the problem of freeing memory twice. Imagine if both s2 and s1 pointed to the String, and then the closing } is reached, so the two variables go out of scope and are freed. The String would be freed twice, which is a serious issue. Instead, since s2 now has ownership, the String is only freed once when s2 goes out of scope, when s1 goes out of scope nothing happens.

A move like this doesn't just happen when you set a variable, it also happens when you call a function with a non-scalar type:

fn main() {
    let x = 4;
    println!("{} squared is {}", x, squared(x));
}

fn squared(n: i32) -> i32 {
    n * n
}

This will print 4 squared is 16 as expected, 4 is an integer, a scalar type, so there is no problem of ownership here.

fn main() {
    let myHeapString = String::from("Hello");
    println!("{} uppercase is {}", myHeapString, uppercase(myHeapString));
}

fn uppercase(text: String) -> String {
    text.to_uppercase()
}

This will result in a compiler error cannot move out of myHeapString because it is borrowed. This tells us two things, first that calling a function with an argument that is not scalar will try to move that argument, so myHeapString would no longer be allowed to be referenced. It also tells us that myHeapString is borrowed and borrowing is also the solution for our problem here. Instead of moving myHeapString into the uppercase function, we modify the function so that it borrows it instead of moving it.

fn main() {
    let myHeapString = String::from("Hello");
    println!("{} uppercase is {}", myHeapString, uppercase(&myHeapString));
}

fn uppercase(text: &String) -> String {
    text.to_uppercase()
}

This will print Hello uppercase is HELLO correctly, no compile error. The only change here is adding & in front of myHeapString in the function call to uppercase, and in front of String in the uppercase function signature. You might know this from C/C++, the & is the reference operator and allows you to pass variables by reference to a function. In Rust it's the same, but the concept of borrowing goes further than that to prevent common bugs like dangling pointers and data races.

As mentioned before, Rust is immutable by default and references are as well. So if you pass a variable to a function by reference (in Rust terms, if the function borrows the variable), the function can read but not modify the value. If you want to be able to modify the value, you use &mut instead of just &, then you have a mutable reference. How does that prevent bugs? The compiler enforces a set of rules:

  1. There can be as many immutable references as you like
  2. There can only be one mutable reference
  3. There cannot be mutable and immutable references at the same time

The 1st rule is clear, if a variable is immutable it doesn't matter how many references point to it, the value won't change, the pointers will always be valid.

The 2nd rule is where it gets interesting. If you had two mutable references and one would free the space of the variable, the other reference would still point to the memory which would now be a dangerous bug, a dangling pointer. By only allowing one mutable reference, that cannot happen.

The 3rd rule also makes sense immediately, the immutable references you have assume that the value they are pointing to cannot change, but a mutable reference could do that, so you're not allowed to create one when there are immutable references.

This might seem a bit limiting at first, but it completely prevents the most common bugs that result in serious security issues because in Rust it leads to a compile error.

Cargo

Rust comes with a package manager called Cargo that is extremely easy to use. It starts simple and you can add as much complexity as you need, which is exactly how tools should be but rarely are.

You can create new projects with cargo by just doing cargo new myapp. It then creates a project folder for you with a simple hello world in src/main.rs and the configuration for cargo in cargo.toml:

[package]
name = "myapp"
version = "0.1.0"
authors = ["CrypticWyrm"]

[dependencies]

To add dependencies (called crates), you just list them under [dependencies]:

[dependencies]
crateone = "^0.10.0"
cratetwo = "^0.20.0"

Those crates then get downloaded and compiled automatically when you for example use cargo build to build your project, or cargo run to both build and run your project.

Rustup

Rust also comes with a tool called rustup, or rather, rustup comes with Rust since you use rustup to install Rust. It's like nvm from the node.js world. It lets you install different versions of Rust, including the nightly build and can also install additional tools like Clippy which is a linter for Rust code, or the Rust Language Server which can be used by text editors to implement things like auto completion. It's easy to use and I really like that this is the default way of installing Rust, it solves a lot of issues that other languages still suffer from even if such a tool is available for them, because the idea of such a tool came up only much later so it's not "standard" which means not everyone uses it.

Cons

I honestly haven't seen much bad things inherent about Rust yet, but maybe it's just too early for that, I'm sure I'll find more as I learn and use Rust more, because whenever I look at big Rust projects like Gnome Podcasts I still am horrified by the complexity. Not as much as I was before learning Rust though, so my perspective has changed at least a little bit and has become more positive.

Executables are freaking huge

This really surprised me. If you compile a simple hello world application, command line only, no GUI, the resulting executable is 4 MB on Linux. Adding a GUI with GTK3 (window with button that prints "hello world" when clicked) still is 4 MB, so that doesn't really add to the size. A C/C++ or Vala version would just be a few kilobytes. Red can do a cross platform native GUI hello world in 1 MB. Google's Go at least manages to keep the command line hello world to about 2 MB. Rust seems to be the Electron of systems programming, it's huge and bloated! Alright, alright, that was a bit tongue in cheek ;) But still, 4 MB seems really wasteful for a systems programming language.

Cryptic keyword abbreviations

Sadly, Rust follows the tradition of C++ to name commonly used keywords using abbreviations, as if we're living in the dark ages without auto complete features in our text editors so having short names would save us some typing. This makes Rust harder to learn and read than it needs to be, for no apparent reason. Examples here are "pub" instead of "public", "mod" instead of "module", "mut" instead of "mutable" and "impl" instead of "implementation". Instead of having to remember the natural English words, you now have to remember the abbreviation Rust's creators arbitrarily came up with in addition to the actual word they refer to so that you can remember the meaning of the words. This is a matter of taste for sure, and historically there were good reasons for using such abbreviations, but in a modern language it feels to me like a step back and not progress. It is however a very minor issue, so I'm nitpicking here.

Nightly builds and unstable features

These are both a pro and a con. It's cool that you can try out new language features, but even though I just started out using Rust I've already had to use nightly to build a library I wanted to use and according to the 2018 Rust survey that is not surprising, because 56% of Rust users use the nightly build, so it's not really optional anymore, you kind of have to use the nightly builds so that you can use unstable features which many cool Rust libraries and tools rely upon. Supposedly the nightly builds tend to be quite stable, so maybe this isn't even a big issue, we'll see :)

Rust links

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!
Sort Order:  

Congratulations! Your post has been selected as a daily Steemit truffle! It is listed on rank 4 of all contributions awarded today. You can find the TOP DAILY TRUFFLE PICKS HERE.

I upvoted your contribution because to my mind your post is at least 8 SBD worth and should receive 156 votes. It's now up to the lovely Steemit community to make this come true.

I am TrufflePig, an Artificial Intelligence Bot that helps minnows and content curators using Machine Learning. If you are curious how I select content, you can find an explanation here!

Have a nice day and sincerely yours,
trufflepig
TrufflePig