blog: start tl;dr rust post

This commit is contained in:
Cadey Ratio 2020-09-19 09:07:30 -04:00
parent 09e1d478df
commit 0677fda161
1 changed files with 536 additions and 0 deletions

View File

@ -0,0 +1,536 @@
---
title: "TL;DR Rust"
date: 2020-09-19
tags:
- rust
- go
- golang
---
# TL;DR Rust
Recently I've been starting to use Rust more and more for larger and larger
projects. As things have come up, I realized that I am missing a good reference
for common things in Rust as compared to Go. This post contains a quick
high-level overview of patterns in Rust and how they compare to patterns
in Go. This will focus on code samples. This is no replacement for the [Rust
book](https://doc.rust-lang.org/book/), but should help you get spun up on the
various patterns used in Rust code.
Also I'm happy to introduce Mara to the blog!
[Hey, happy to be here! I'm Mara, I'll interject with side information,
challenge assertions and more! Thanks for inviting
me!](conversation://Mara/hacker)
Let's start somewhere simple: functions.
## Making Functions
Functions are defined using `fn` instead of `func`:
```go
// go
func foo() {}
```
```rust
// rust
fn foo() {}
```
### Arguments
Arguments can be passed by separating the name from the type with a colon:
```go
// go
func foo(bar int) {}
```
```rust
// rust
fn foo(bar: i32) {}
```
### Returns
Values can be returned by adding `-> Type` to the function declaration:
```go
// go
func foo() int {
return 2
}
```
```rust
// rust
fn foo() -> i32 {
return 2;
}
```
In Rust values can also be returned on the last statement without the `return`
keyword or a terminating semicolon:
```rust
// rust
fn foo() -> i32 {
2
}
```
### Functions that can fail
The [Result](https://doc.rust-lang.org/std/result/) type represents things that
can fail with specific errors. The [eyre Result
type](https://docs.rs/eyre) represents things that can fail
with any error. For readability, this post will use the eyre Result type.
[The angle brackets in the `Result` type are arguments to the type, this allows
the Result type to work across any type you could imagine.](conversation://Mara/hacker)
```go
// go
import "errors"
func divide(x, y int) (int, err) {
if y == 0 {
return 0, errors.New("cannot divide by zero")
}
return x / y, nil
}
```
```rust
// rust
use eyre::{eyre, Result};
fn divide(x: i32, y: i32) -> Result<i32> {
match y {
0 => Err(eyre!("cannot divide by zero")),
_ => Ok(x / y),
}
}
```
[Huh? I thought Rust had the <a
href="https://doc.rust-lang.org/std/error/trait.Error.html">Error trait</a>,
shouldn't you be able to use that instead of a third party package like
eyre?](conversation://Mara/wat)
Let's try that, however we will need to make our own error type because the
[`eyre!`](https://docs.rs/eyre/0.6.0/eyre/macro.eyre.html) macro creates its own
transient error type on the fly.
First we need to make our own simple error type for a DivideByZero error:
```rust
// rust
use std::error::Error;
use std::fmt;
#[derive(Debug)]
struct DivideByZero;
impl fmt::Display for DivideByZero {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "cannot divide by zero")
}
}
impl Error for DivideByZero {}
```
So now let's use it:
```rust
// rust
fn divide(x: i32, y: i32) -> Result<i32, DivideByZero> {
match y {
0 => Err(DivideByZero{}),
_ => Ok(x / y),
}
}
```
However there is still one thing left: the function returns a DivideByZero
error, not _any_ error like the [error interface in
Go](https://godoc.org/builtin#error). In order to represent that we need to
return something that implements the Error trait:
```rust
// rust
fn divide(x: i32, y: i32) -> Result<i32, impl Error> {
// ...
}
```
And for the simple case, this will work. However as things get more complicated
this simple facade will not work due to reality and its complexities. This is
why I am shipping as much as I can out to other packages like eyre or
[anyhow](https://docs.rs/anyhow). Check out this code in the [Rust
Playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=946057d8eb02f388cb3f03bae226d10d)
to mess with this code interactively.
[Pro tip: eyre (via <a href="https://docs.rs/color-eyre">color-eyre</a>) also
has support for adding <a href="https://docs.rs/color-eyre/0.5.4/color_eyre/#custom-sections-for-error-reports-via-help-trait">custom
sections and context</a> to errors similar to Go's <a href="https://godoc.org/fmt#Errorf">`fmt.Errorf` `%w`
format argument</a>, which will help in real world
applications. When you do need to actually make your own errors, you may want to look into
crates like <a href="https://docs.rs/thiserror">thiserror</a> to help with
automatically generating your error implementation.](conversation://Mara/hacker)
### The `?` Operator
In Rust, the `?` operator checks for an error in a function call and if there is
one, it automatically returns the error and gives you the result of the function
if there was no error. This only works in functions that return a Result.
```go
// go
func doThing() (int, error) {
result, err := divide(3, 4)
if err != nil {
return 0, err
}
return result, nil
}
```
```rust
// rust
use eyre::Result;
fn do_thing() -> Result<i32> {
let result = divide(3, 4)?;
Ok(result)
}
```
If the second argument of divide is changed to `0`, then `do_thing` will return
an error.
[And how does that work with eyre?](conversation://Mara/hmm)
It works with eyre because eyre has its own error wrapper type called
[`Report`](https://docs.rs/eyre/0.6.0/eyre/struct.Report.html), which can
represent anything that implements the Error trait.
## Macros
Rust macros are function calls with `!` after their name:
```rust
// rust
println!("hello, world");
```
## Variables
Variables are created using `let`:
```go
// go
var foo int
var foo = 3
foo := 3
```
```rust
// rust
let foo: i32;
let foo = 3;
```
### Mutability
In Rust, every variable is immutable (unchangeable) by default. To create a
mutable variable, add the `mut` keyword after the `let` keyword. There is no
analog to this in Go.
```rust
let mut i: i32 = 0;
i = i + 5;
```
[This is slightly a lie. There's more advanced cases involving interior
mutability and other fun stuff like that, however this is a more advanced topic
that isn't covered here.](conversation://Mara/hacker)
### Lifetimes
Rust does garbage collection at compile time. It also passes ownership of memory
to functions as soon as possible. For example:
```rust
let quo = divide(4, 8)?;
let other_quo = divide(quo, 5)?;
// Fails compile because quo was given to divide to create other_quo
let yet_another_quo = divide(quo, 4)?;
```
To work around this you need to either clone the value or pass a reference:
```rust
let other_quo = divide(quo.clone(), 5);
let yet_another_quo = divide(quo, 4)?;
```
To pass a reference to a function, use the `&` character:
```
let something = do_something(&quo)?;
```
### Passing Mutability
Sometimes functions need mutable variables. To pass a mutable reference, add
`&mut` before the name of the variable:
```rust
let something = do_something_to_quo(&mut quo)?;
```
## Project Setup
### Imports
External dependencies are declared using the [Cargo.toml
file](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html):
```toml
[dependencies]
anyhow = "1.0"
```
This depends on the crate [anyhow](https://crates.io/anyhow) at version 1.0.x.
Dependencies can also have optional features:
```toml
[dependencies]
reqwest = { version = "0.10", features = ["json"] }
```
This depends on the crate [reqwest](https://crates.io/reqwest) at version 0.10.x
with the `json` feature enabled (in this case it enables reqwest being able to
automagically convert things to/from json using Serde).
External dependencies can be used with the `use` statement:
```go
import "github.com/foo/bar"
```
```rust
use foo; // -> foo now has the members of crate foo behind the :: operator
use foo::Bar; // -> Bar is now exposed as a type in this file
use anyhow::{anyhow, Result}; // exposes the anyhow! and Result members of anyhow
```
## Async/Await
Async functions may be interrupted to let other things execute as needed. This
program uses [tokio](https://tokio.rs/) to handle async tasks. To run an async
task and wait for its result, do this:
```
let response = reqwest::get("https://printerfacts.cetacean.club/fact")
.await?
.text()
.await?;
```
This will populate `response` with the HTML source of https://within.website.
To make an async function, add the `async` keyword before the `fn` keyword:
```rust
async fn get_html(url: String) -> Result<String> {
reqwest::get(&url)
.await?
.text()
.await?
}
```
This can then be called like this:
```rust
let within_website_html = get_html("https://within.website").await?;
```
## Public/Private Types and Functions
Rust has three privacy levels for functions:
- Only visible to the current file (no keyword, lowercase in Go)
- Visible to anything in the current crate (`pub(crate)`, internal packages in
go)
- Visible to everyone (`pub`, upper case in Go)
This project will mostly use `pub(crate)` as none of this code is intended to be
consumed by other programs (though this may change in the future).
## Structures
Rust structures are created using the `struct` keyword:
```go
type Client struct {
Token string
}
```
```rust
pub(crate) struct Client {
pub token: String,
}
```
If the `pub` keyword is not specified before a member name, it will not be
usable outside the Rust source code file it is defined in:
```go
type Client struct {
token string
}
```
```rust
pub(crate) struct Client {
token: String,
}
```
### Encoding structs to JSON
[serde](https://serde.rs) is used to convert structures to json. The Rust
compiler's
[derive](https://doc.rust-lang.org/stable/rust-by-example/trait/derive.html)
feature is used to automatically implement the conversion logic.
```go
type Response struct {
Name string `json:"name"`
Description *string `json:"description,omitempty"`
}
```
```rust
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
pub(crate) struct Response {
pub name: String,
pub description: Option<String>, // Option means that there can either be something or nothing there
}
```
## Strings
Rust has a few string types that do different things. You can read more about
this [here](https://fasterthanli.me/blog/2020/working-with-strings-in-rust/),
but at a high level this project only uses two of them:
- String, an owned UTF-8 string
- PathBuf, a filepath string (encoded in whatever encoding the OS running this
code uses for filesystems)
The strings are different types for safety reasons. See the linked blogpost for
more detail about this.
## Enumerations / Tagged Unions
Enumerations, also known as tagged unions, are a way to specify a superposition
of one of a few different kinds of values in one type. The main place they are
used in this project is for command line parsing with
[structopt](https://docs.rs/structopt/0.3.14/structopt/). There is no easy
analog for this in Go.
```rust
#[derive(StructOpt, Debug)]
#[structopt(about = "A simple release management tool")]
pub(crate) enum Cmd {
/// Creates a new release for a git repo
Cut {
#[structopt(flatten)]
common: Common,
/// Changelog location
#[structopt(long, short, default_value="./CHANGELOG.md")]
changelog: PathBuf,
},
/// Runs releases as triggered by GitHub Actions
GitHubAction {
#[structopt(flatten)]
gha: GitHubAction,
},
}
```
Enum variants can be matched using the `match` keyword:
```rust
match cmd {
Cmd::Cut { common, changelog } => {
cmd::cut::run(common, changelog).await
}
Cmd::GitHubAction { gha } => {
cmd::github_action::run(gha).await
}
}
```
All variants of an enum must be matched in order for the code to compile.
## Testing
Test functions need to be marked with the `#[test]` annotation, then they will
be run alongside `cargo test`:
```rust
mod tests { // not required but it is good practice
#[test]
fn math_works() {
assert_eq!(2 + 2, 4);
}
#[test]
async fn http_works() {
let _ = get_html("https://within.website").await.unwrap();
}
}
```
Avoid the use of `unwrap()` outside of tests. In the wrong cases, using
`unwrap()` in production code can cause the server to crash and can incur data
loss.
---
This is by no means comprehensive, see the rust book or [Learn X in Y Minutes
Where y = Rust](https://learnxinyminutes.com/docs/rust/) for more information.
This code is written to be as boring and obvious as possible. If things don't
make sense, please reach out and don't be afraid to ask questions.