So I tried Rust for the first time.
Tags: rust
Published: 2024-06-04 17:11:42

---
title: So I tried Rust for the first time.
published: true
description: My first attempt at writing a program in rust.
tags: rust, rustlang
cover_image: https://www.rust-lang.org/static/images/rust-social-wide.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2024-06-04 16:01 +0000
---

In my professional life, I'm at home on the Java Virtual Machine. I started out in Java over 10 years ago, but since around 4 years, I'm programming almost entirely in Kotlin. And while Kotlin serves a lot of different use cases and compilation targets, I've already felt like I need a "native tool" in my tool belt. I can deal with C if I have to, but it feels very dated at this point. And I heard all the buzz about Rust being the most loved programming language out there, so I thought I might take a look and see for myself.

Here's how that went.

# Installation

The installation process was super smooth on my linux machine. I've installed `rustup` and let `cargo init` do its job; project setup done. I started my Visual Studio Code, got a few plugins, and was hacking away within minutes. I especially love the fact that `cargo` is also a built-in dependency management tool. Nice!

# Advent of Code

Whenever I try a new language, the first thing I throw it at is usually the [Advent of Code](https://adventofcode.com/). It is a set of language-agnostic problems which start out very easy and gradually get more difficult; perfect for testing a new language!

I picked the "Day 1" example of 2023. We're given a textual input file; each line contains digits and characters. We need to find the first and the last digit in each row, convert them into a number (e.g. 8 and 3 turn into 83) and sum them up. After a little bit of tinkering, I ended up with this:

```rs
static INPUT: &'static str = include_str!("inputs/1.txt");

fn main() {
    let mut sum = 0;
    for line in INPUT.lines() {
        let numbers: Vec<u32> = line
            .chars()
            .filter(|c| c.is_numeric())
            .map(|c| c.to_digit(10).unwrap())
            .collect();
        let first = numbers.first().unwrap();
        let last = numbers.last().unwrap();
        sum += first * 10 + last;
    }
    println!("{}", sum);
}
```

This ended up producing the correct result. This may not be idiomatic rust (I have yet to figure out what that is for myself) but it gets the job done. I have a lot of things to say about this little program.

## include_str!

We're already starting out with a **macro** on the first line. I'm REALLY not a fan of macros[^1], but that's just how Rust rolls I guess. If we look beyond the problems of macros overall, the `include_str!` macro is really nice. It includes the text of an input file as a string in the program, and the compiler verifies that the file path is correct and the file exists. This should raise some eyebrows: this macro is doing more than just producing regular Rust code, it talks to the compiler. This pattern of macros opening sneaky paths to compiler intrinsics is going to repeat on our journey. It allows the compiler to provide better error messages, but macros are also really good at hiding what code is actually being executed. At the very least, in Rust all macros are clearly demarcated with a trailing `!` in their name so you know where the "magic" is[^2].

## Give me a lifetime

Right in our first line we're also hit with the expression `&'static`. There are really two things going on here:

- `&` means that the type that comes next (`str`) is actually a "borrowed" reference. We will dig into this later, for now know that a borrowed thing is immutable.

- `'static` is a "lifetime specifier". All of these start with a single quote (`'`).  Rust is the only language I've ever seen that uses single quotes in a non-pair-wise fashion. And **good lord is it ugly**! Looking past the syntax, `'static` is a special lifetime that means "lives for the entire duration of the program", which makes sense for a constant like this.

## main

The main function declaration is nothing special, aside from the nice fact that you're not forced to accept parameters, nor are you forced to return an integer (looking at you, Java!). `fn` is fine as a keyword, but I still prefer Kotlin's `fun`.

## let mut

Variables are declared with `let`, types can optionally be annoated after the name with `: SomeType`. Fairly standard, all fine. The keyword `mut` allows us to mutate the variable after we've defined it; by default, all `let`s are immutable. Kotlin's `var` and `val` approach is a little more elegant in that regard, but it's fine.

## Semicolon!! `;`

Gods why. That's all I have to say. Why does the darn thing refuse to die? Chalk up another language with mandatory semicolons, they just keep coming.

## for

Next is our standard "foreach" loop. A noteworthy detail is that there are no round braces `( ... )`, which is also true for `if` statements. Takes a little time to get used to, but works for me. `lines()` splits the text into individual lines for us.

## Iterators

Next we see `chars()` which gives us an iterator over the characters in the current line. Rust actually doesn't differentiate between a lazy `Sequence` and a lazy `Iterator`, like Kotlin does. So we have all the functional good stuff directly on the `Iterator`. We `filter` for the numeric characters, we `map` them to integers, we `collect` the result in a `Vec<u32>` (which is roughly comparable to a `List<Int>` in kotlin).

## Lambdas

The lambda syntax in rust is... iffy at best, and a monstrosity at worst. The examples in the `filter` and `map` calls are very basic ones, things can get much worse. Rust actually has lambdas with different semantics (kind of like kotlin's `callsInPlace` contract). Sure, these things are there to aid the borrow checker, but I really miss my `it` from kotlin. `filter { it.isDigit() }` is hard to beat in terms of readability.

## Type Inference... sometimes.

Much like the Kotlin compiler, the Rust compiler is usually pretty clever at figuring out the types of local variables and such so you don't have to type them all the time. Except that my `let numbers` absolutely required a manual type annotation for some reason. It's not like `collect()` could produce anything else in this example, so I don't really get why.

> EDIT: Esfo pointed out in the comments (thanks!) that `collect()` can be used to produce more than just `Vec<T>` as output. Rust in this case determines the overload of `collect()` based on the **expected return type** - which is wild! I'm not sure if I like it or not.

## Unwrap all the things!

You may have noticed the calls to `unwrap()`. The method `unwrap()` is defined on Rust's `Result<T, E>` type, which can be either an `Ok<T>` (success) or an `Err<E>` (error). `unwrap()` on `Ok` simply returns the value, `unwrap()` on `Err` causes a panic and terminates the whole program with an error message. And no, there is no `try{ ... } catch { ... }` in Rust. Once your program panics, there's no turning back. So `unwrap()` shouldn't be used in production code. For this simple toy example, I think it's good enough. Later I learned that if you're inside a method that returns a `Result`, you can also use `?` to propagate the error immediately.[^3]

## print it!

Rust wouldn't be Rust if `println` was straight forward, because it really isn't. Right off the bat we see that `println!` has an exclamation mark, so it's a macro. And oh boy does it do a lot of magic. Rust doesn't **really** have string interpolation, so the standard way to print something is `println!("foo {} bar", thing)` which will first print `foo`, then the `thing`, then `bar`. That is, provided that `thing` is printable, but that's another story. You **could** also write `println!("foo {thing} bar")` which **almost** looks like string interpolation, but it's really just a feature of the `println!` macro. It breaks down quickly, for example when you try to access a field inside the curly brackets. This won't compile: `println!("foo {thing.field} bar")`. So while it may **seem** simple at first glance, it really isn't and it does a whole lot of magic. Kotlin string templates are not without issues (especially if you want a literal `$` character in the string) but they are more universal because they allow for arbitrary logic in their placeholders. 


# The Pros and Cons

At this point, it's time for a first verdict.

- Pro: Tooling is good
- Pro: Compiles natively
- Pro: Memory safe without manual memory management or garbage collector
- Pro: small executables, excellent runtime performance
- Pro: Feels "modern"


- Con: Syntax. Good lord the syntax.
- Con: Macros. No need to explain that.
- Con: Error handling, or rather the absence thereof
- Con: Enormously steep learning curve with borrowing and lifetimes
- Con: Type inference sometimes works, sometimes doesn't

... and that's just for the first example. I've since performed more extensive experiments with the language, and I'll report about these in another article.

I can see why system level programmers enjoy Rust. It's certainly a big step up from C. But on a grand scale, compared to the effectiveness and productivity of Kotlin, I can't yet see where Rust's popularity is really coming from.

[^1]: As programmers, we already have enough tools at our disposal to shoot ourselves into the foot; macros just add to that danger.
[^2]: In C, that's not the case. You can make a macro look like a totally normal function call, but it does arbitrary other things when compiled.
[^3]: The Go language should take notes here. `if(err != nil) return err` is only funny so many times before it gets really old really fast.