Rust
for
Non-Systems Programmers

Rebecca Turner (she/her)

@16kbps / becca.ooo

Why this talk?

A screenshot of the rust-lang.org website in late 2018. The headline reads "Rust is a systems programming language that runs blazingly fast, prevents segfaults, and guarantees thread safety."
A screenshot of the rust-lang.org website in mid-2020. The headline reads "A language empowering everyone to build reliable and efficient software." and sections under "Why Rust?" emphasize performance, reliability, and productivity.
Twitter user @16kbps's avatar
rebecca
@16kbps
hi my names rebecca and i still dont understand rust
10:12 AM · May 1, 2017
2Likes

What can Rust
do for you?

rustconf-code 0.1.0
A command-line interface to the openweathermap.org API

USAGE:
    rustconf-code [OPTIONS]

FLAGS:
    -h, --help       Prints help information
    -V, --version    Prints version information

OPTIONS:
    -c, --config <config>    Config filename; a JSON file with
                             an `api_key` field
                             [default: openweather_api.json]
$ ./target/debug/rustconf-code --confiig cfg.json
error: Found argument '--confiig' which wasn't expected,
       or isn't valid in this context
       Did you mean --config?

USAGE:
    rustconf-code --config <config>

For more information try --help
$ ./target/debug/rustconf-code --config bad-schema.json
Error: Failed to deserialize configuration JSON

Caused by:
    invalid type: map, expected a string at line 2 column 14
running 1 test
test tests::str_test ... FAILED

failures:
---- tests::str_test stdout ----
thread 'tests::str_test' panicked at
'assertion failed: `(left == right)`

Diff < left / right > :
<"Hello, RustConf!"
>"Hello, RustConf 2020!"

', src/main.rs:11:9

test result: FAILED. 0 passed; 1 failed; 0 ignored;
0 measured; 0 filtered out

(And a lot more)

Let’s write a program

What is this talk not?

Why do I like Rust?

Tooling

Documentation

Getting started

use std::env;

fn main() {
    let user = env::var("USER").unwrap();
    if user == "becca" {
        println!("Hello, Rebecca!");
    } else {
        println!("Hello, {}!", user);
    }
}
$ cargo build
   Compiling rustconf-code v0.1.0 (~/rustconf/rustconf-code)
    Finished dev [unoptimized + debuginfo] target(s) in 0.17s
$ ./target/debug/rustconf-code
Hello, Rebecca!
$ USER=nell ./target/debug/rustconf-code
Hello, nell!
$ env USER= ./target/debug/rustconf-code
Hello, !
$ env USER="$(printf '\xc3\x28')" ./target/debug/rustconf-code
thread 'main' panicked at 'called `Result::unwrap()` on an
`Err` value: NotUnicode("\xC3(")', src/main.rs:4:16
note: run with `RUST_BACKTRACE=1` environment variable to display
a backtrace
enum Result<T, E> {
    Ok(T),
    Err(E),
}
use std::env;

fn main() {
    let user = env::var("USER");
    match user {
        Ok(user) => {
            if user == "becca" {
                println!("Hello, Rebecca!");
            } else {
                println!("Hello, {}!", user);
            }
        }
        Err(_) => println!("I couldn't figure out who you are!"),
    }
}
$ env USER="$(printf '\xc3\x28')" ./target/debug/rustconf-code
I couldn't figure out who you are!
A Star TSP100 Eco futurePRNT 72mm receipt printer, powered on with a printed receipt showing the RustConf homepage reading "Beaming to screens across the globe"
import json

import requests

with open("openweather_api.json") as f:
    api_key_obj = json.load(f)
    api_key = api_key_obj["api_key"]
    res = requests.get(
        "https://api.openweathermap.org/data/2.5/weather",
        params={"q": "Waltham,MA,US", "appid": api_key},
    )
    print(res.text)
$ ./openweather.py
{"coord":{"lon":-71.24,"lat":42.38},"weather":[{"id":801,"main":
"Clouds","description":"few clouds","icon":"02d"}],"base":
"stations","main":{"temp":298.14,"feels_like":297.91,"temp_min":
296.48,"temp_max":299.82,"pressure":1009,"humidity":57}, ...
use serde_json::Value;

fn main() {
    let api_key_json = include_str!("../openweather_api.json");
    let api_key_obj: Value =
        serde_json::from_str(api_key_json).unwrap();
    let api_key = api_key_obj
        .as_object()
        .unwrap()
        .get("api_key")
        .unwrap()
        .as_str()
        .unwrap();
    println!("API key is {}", api_key);
}
use serde::Deserialize;

#[derive(Debug, Clone, Deserialize)]
struct OpenWeatherConfig {
    api_key: String,
}
trait From<T> {
  fn from(T) -> Self;
}

impl From<String> for OpenWeatherConfig {
  fn from(key: String) -> Self {
    Self {
      api_key: key,
    }
  }
}
trait Into<T> {
  fn into(self) -> T;
}

impl<T, U> Into<U> for T
where
  U: From<T>,
{
  fn into(self) -> U {
    U::from(self)
  }
}
impl From<String> for OpenWeatherConfig {
  fn from(key: String) -> Self {
    Self {
      api_key: key,
    }
  }
}

let key: String = "92c2321c2e66af5bb3aaf07bdbfe9cb7".to_owned();
let cfg: OpenWeatherConfig = key.into();
let key: &str = "92c2321c2e66af5bb3aaf07bdbfe9cb7";
let key_owned: String = key.to_owned();
use serde::Deserialize;

#[derive(Debug, Clone, Deserialize)]
struct OpenWeatherConfig {
    api_key: String,
}
use serde::Deserialize;

fn main() {
    let config_json = include_str!("../openweather_api.json");
    let config: OpenWeatherConfig =
        serde_json::from_str(config_json).unwrap();
    println!("{:?}", config);
}
$ cat openweather_api.json
{
  "api_key": "1b13e6aa173ce14137a50095476e653c"
}

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.05s
     Running `target/debug/rustconf-code`
OpenWeatherConfig {
    api_key: "1b13e6aa173ce14137a50095476e653c"
}
use std::path::PathBuf;

use structopt::StructOpt;

/// A command-line interface to the openweathermap.org API.
#[derive(Debug, StructOpt)]
struct Opt {
    /// Config filename; a JSON file with an `api_key` field.
    #[structopt(short, long, parse(from_os_str))]
    config: PathBuf,
}

fn main() {
    let opt = Opt::from_args();
    // ...
}
$ ./target/debug/rustconf-code --help
rustconf-code 0.1.0
A command-line interface to the openweathermap.org API

USAGE:
    rustconf-code --config <config>

FLAGS:
    -h, --help       Prints help information
    -V, --version    Prints version information

OPTIONS:
    -c, --config <config>    Config filename; a JSON file with
                             an `api_key` field
$ ./target/debug/rustconf-code --confgi my-custom-file.json
error: Found argument '--confgi' which wasn't expected, or isn't
       valid in this context
       Did you mean --config?

USAGE:
    rustconf-code --config <config>

For more information try --help


fn main() -> eyre::Result<()> {
    let opt = Opt::from_args();
    let config_json = File::open(&opt.config)?;
    let config: OpenWeatherConfig =
        serde_json::from_reader(&config_json)?;
    println!("Config: {:#?}", config);
    Ok(())
}


fn main() -> eyre::Result<()> {
    let opt = Opt::from_args();
    let config_json =
        match File::open(&opt.config) {
            Ok(file) => file,
            Err(err) => return Err(err.into()),
        };
    let config: OpenWeatherConfig =
        match serde_json::from_reader(&config_json) {
            Ok(config) => config,
            Err(err) => return Err(err.into()),
        };
    println!("Config: {:#?}", config);
    Ok(())
}
use eyre::WrapErr;

fn main() -> eyre::Result<()> {
    let opt = Opt::from_args();
    let config_json =
        File::open(&opt.config).wrap_err_with(|| {
            format!(
                "Failed to open config file {:?}",
                opt.config
            )
        })?;
    let config: OpenWeatherConfig =
        serde_json::from_reader(&config_json)
            .wrap_err("Failed to deserialize JSON")?;
    println!("Config: {:#?}", config);
    Ok(())
}
$ ./target/debug/rustconf-code --config nonexistent-file.json
Error: Failed to open config file "nonexistent-file.json"

Caused by:
    No such file or directory (os error 2)

$ ./target/debug/rustconf-code --config invalid-file.json
Error: Failed to deserialize JSON

Caused by:
    trailing comma at line 3 column 1
use reqwest::blocking::{Client, Response};

fn get_weather(
    api_key: &str,
) -> Result<Response, reqwest::Error> {
    let client = Client::new();
    client
        .get("https://api.openweathermap.org/data/2.5/weather")
        .query(&[("q", "Waltham,MA,US"), ("appid", api_key)])
        .send()
}
println!("Response: {:#?}", get_weather(&config.api_key)?);
$ cargo run
Response: Response {
    url: "https://api.openweathermap.org/data/2.5/weather?q=...",
    status: 200,
    headers: {
        "server": "openresty",
        "date": "Sun, 19 Jul 2020 19:30:04 GMT",
        "content-type": "application/json; charset=utf-8",
        "content-length": "465",
        "connection": "keep-alive",
        "x-cache-key": "/data/2.5/weather?q=waltham%2cma%2cus",
    },
}
let res = get_weather(&config.api_key)?;
let bytes = res.bytes()?;
println!("{}", String::from_utf8_lossy(&*bytes));
{"coord":{"lon":-71.24,"lat":42.38},"weather":[{"id":802,
"main":"Clouds","description":"scattered clouds","icon":
"03d"}],"base":"stations","main":{"temp":308.71,"feels_like":
307.42,"temp_min":307.59,"temp_max":309.82,"pressure":1010,
"humidity":37},"visibility":10000,"wind":{"speed":6.2, ...
use reqwest::blocking::Client;
use serde::Deserialize;

#[derive(Deserialize, Debug, Clone)]
struct OpenWeather {
    api_key: String,

    lat: f64,
    lon: f64,

    #[serde(skip)]
    client: Client,
}
fn main() -> eyre::Result<()> {
    // ...
    let config: OpenWeather = serde_json::from_reader(
        &config_json,
    )?;
    // ...
}
impl OpenWeather {
    fn get<Response: DeserializeOwned>(
        &self,
        endpoint: &str,
        params: &[(&str, &str)],
    ) -> eyre::Result<Response> {
      // I have discovered a truly marvelous implementation
      // of this method, which this slide is too narrow
      // to contain.
    }
}
#[derive(Deserialize, Debug, Clone)]
pub struct OneCall {
    pub hourly: Vec<Hourly>,
    pub daily: Vec<Daily>,
}

#[derive(Deserialize, Debug, Clone)]
pub struct Hourly {
    pub dt: UnixUTC,
    pub temp: f64,
    pub feels_like: f64,
    pub humidity: f64,
    pub clouds: f64,
    pub rain: Option<Rain>,
    pub snow: Option<Snow>,
}
impl OpenWeather {
    fn onecall(&self) -> eyre::Result<OneCall> {
        self.get(
            "onecall",
            &[ ("exclude", "currently,minutely"),
                ("units", "imperial"), ],
        )
    }
}

fn main() -> eyre::Result<()> {
    // ...
    let onecall: OneCall = config
        .onecall()
        .wrap_err("Failed to deserialize hourly weather data")?;
}
#[derive(Debug, PartialEq)]
enum TempDifference {
    MuchColder,
    Colder,
    Same,
    Warmer,
    MuchWarmer,
}
impl TempDifference {
    fn from(from: f64, to: f64) -> Self {
        let delta = to - from;
        match delta {
            d if d >  10.0 => TempDifference::MuchWarmer,
            d if d >   5.0 => TempDifference::Warmer,
            d if d < -10.0 => TempDifference::MuchColder,
            d if d <  -5.0 => TempDifference::Colder,
            _ => TempDifference::Same,
        }
    }
}
#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_tempdiff() {
        assert_eq!(
            TempDifference::from(50.0, 69.0),
            TempDifference::MuchWarmer
        );
        // And so on, but these slides are small...
    }
}
$ cargo test
    Finished test [unoptimized + debuginfo] target(s)
             in 0.04s
     Running target/debug/deps/rustconf_code-affd1f0e8

running 1 test
test test::test_tempdiff ... ok

test result: ok. 1 passed; 0 failed; 0 ignored;
                 0 measured; 0 filtered out
struct Stats {
    min: f64,
    max: f64,
    avg: f64,
    count: usize,
}
impl Default for Stats {
    fn default() -> Self {
        Self {
            min: f64::INFINITY,
            max: f64::NEG_INFINITY,
            avg: 0.0,
            count: 0,
        }
    }
}
impl Stats {
    fn from(itr: impl Iterator<Item = f64>) -> Self {
        let mut ret = Self::default();
        let mut sum = 0.0;
        // ...
    }
}
impl Stats {
    fn from(itr: impl Iterator<Item = f64>) -> Self {
        // ...
        for i in itr {
            if i < ret.min {
                ret.min = i;
            } else if i > ret.max {
                ret.max = i;
            }
            ret.count += 1;
            sum += i;
        }
        // ...
    }
}
impl Stats {
    fn from(itr: impl Iterator<Item = f64>) -> Self {
        // ...
        ret.avg = sum / ret.count as f64;
        ret
    }
}
let yesterday =
    Stats::from(historical.iter().map(|h| h.feels_like));
let today = Stats::from(
    onecall.hourly.iter().map(|h| h.feels_like).take(24),
);

let diff = TempDifference::from(yesterday.avg, today.avg);
let today_is_warm = 60.0 <= today.avg && today.avg <= 80.0;
print!("Good morning! Today will be about {:.2}°F ", today.avg);
println!(
    "({min} - {max}°F); that's {diff} {than} yesterday{end}",
    min = today.min,
    max = today.max,
    diff = diff,
    than = match diff {
        TempDifference::Same => "as",
        _ => "than",
    },
    end = if today_is_warm { " :)" } else { "." },
);
$ cargo build
    Finished dev [unoptimized + debuginfo] target(s) in 0.04s

$ ./target/debug/rustconf-code
Good morning! Today will be about 85.16°F (76.42 - 94.96°F);
that's about the same as yesterday.
$ ./target/debug/rustconf-code | lp
request id is star-22 (0 file(s))
Me holding a printed receipt in my hand, which reads "Good morning! Today will be about 77.76°F (69.3 - 89.76°F); that's about the same as yesterday :)

Don’t be afraid to .clone()

This is only a taste

Rust
for
Non-Systems Programmers

Rebecca Turner (she/her)

@16kbps / becca.ooo