Skip to main content

Mike Kreuzer

Revisiting Rust

12 March 2021

Almost five years ago now in April 2016 I had a look at serialising a struct into JSON in a couple of different languages, including Rust. It feels past time to go back over that & have a look at what's changed with my inexpert eye, because what's changed with Rust turns out to be quite a bit, and so far it's all for the better.

The post, including the Rust code is still there, but before you go clicking on that link, I'll give you the code here to compare. This is not a clickbait driven website. Have you read this website? This is the opposite of a clicks driven site. I actively drive readers away.

extern crate rustc_serialize;
use rustc_serialize::json;
use std::error::Error;
use std::io::prelude::*;
use std::fs::File;
use std::path::Path;

#[derive(RustcDecodable, RustcEncodable)]
pub struct LangStruct {
    name: String,
    url: String,
    countStr: String, //count_str is canonical
    count: u8,
}

fn main() {
    let obj_array: [LangStruct; 2] = [
        LangStruct{
            name: "Ada".to_string(),
            url: "https://www.reddit.com/r/ada/".to_string(),
            countStr: "0".to_string(),
            count: 0
        },
        LangStruct{
            name: "C".to_string(),
            url: "https://www.reddit.com/r/C_Programming".to_string(),
            countStr: "0".to_string(),
            count: 0
        },
    ];
    let encoded = json::encode(&obj_array).unwrap();

    let path = Path::new("small.json");
    let mut file = match File::create(&path) {
        Err(why) => panic!("{}", Error::description(&why)),
        Ok(file) => file,
    };

    match file.write_all(encoded.as_bytes()) {
        Err(why) => panic!("{}", Error::description(&why)),
        Ok(_) => println!("done"),
    };
}

Firstly it's worth noting, that Rust code from five years ago, that still compiles just fine. Things have changed in the language, there are deprecation warnings, but five year old code and it still works. There is no Swift code on the planet that lasts anywhere near five years untouched. None. That is quite the achievement for the Rust folks.

So what has changed?

The serde crate (serde, named after serialise/deserialise) has replaced rustc_serialize. And I don't know if I knew about it before or not, but enabling the features of a crate is a thing now in cargo, the magic words being:

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

It took me a lot of googling to work that out, code examples, and examples covering changes especially are still lacking. Maybe it's too niche a thing, but blog posts like this one I could not find.

The use declarations then become:

extern crate serde;
extern crate serde_json;
use std::fs::File;
use std::io::prelude::Write;
use std::path::Path;
use serde::{Deserialize, Serialize};

Something I didn't comment on (or maybe think of) last time, but use std::io::prelude::* or Write, to include parts of other crates, that seems like a fail to me. Maybe wrongly, this is an outsiders view. It makes me think of macros too. To rely so heavily on them, I think because the compiler can't deal with variable numbers of function arguments? I don't know. I don't know enough about building compilers to really say if it could be done differently, but still be as safe and as fast. Clearly not so far.

Structs haven't changed, as far as I can tell, other than calling a different crate to use on them here:

#[derive(Serialize, Deserialize)]
pub struct LangStruct {
    name: String,
    url: String,
    count_str: String,
    count: u8,
}

Which brings us to the main function. In the past it seems it would have been much faster to call to_owned() rather than to_string(). My bad, I didn't know. Apparently now to string is every bit as fast though, and rather more meaningful:

fn main()
{
    let obj_array: [LangStruct; 2] = [
        LangStruct {
            name: "Ada".to_string(),
            url: "https://www.reddit.com/r/ada/".to_string(),
            count_str: "0".to_string(),
            count: 0,
        },
        LangStruct {
            name: "C".to_string(),
            url: "https://www.reddit.com/r/C_Programming".to_string(),
            count_str: "0".to_string(),
            count: 0,
        },
    ];

But the thing that's excited me is how much less verbose the language seems to have become, at least with this example, which is only about a third as long now:

    let encoded = serde_json::to_string_pretty(&obj_array).expect("JSON to string failed");

    let path = Path::new("small.json");
    let mut file = File::create(&path).expect("File create failed");
    file.write_all(encoded.as_bytes())
        .expect("File write failed");
}

.except() here is unwrapping an optional, but with a custom message if it fails. Rather nicer than a ton of nested if let statements in Swift, to my mind. Pattern matching against the error still works, though the format of writing the error string out has changed a bit if you use that approach.

Finally, there's a pretty print option now too, to_string_pretty, shown here. Call to_string for the minified output of old.

So, some rather nice changes, plenty to like, a lot still to explore. Rust has been called "fractally complex", which I love as a description of anything, and who knows, that may well be true. But I like what I see so far. My revisit may take a while.