Thoughts on Rust

I've been interested in getting back into trying to complete the Gossip Glomers, a set of distributed systems challenges from fly.io. Specifically, I've been playing with frameworks for using Rust to interact with Maelstrom in order to grasp the distinctions between Rust and Golang. To contrast, I've implemented something along the same lines as unique ID generation in Golang.

use maelstrom_common::{run, HandleMessage, Envelope};
use serde::{Deserialize, Serialize};
use core::panic;

#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Message {
    #[serde(rename = "init")]
    Init {
        #[serde(skip_serializing_if = "Option::is_none")]
        msg_id: Option<usize>,
        node_id: String
    },
    #[serde(rename = "generate")]
    Generate {
        #[serde(skip_serializing_if = "Option::is_none")]
        msg_id: Option<usize>
    },
    #[serde(rename = "init_ok")]
    InitOk {
        #[serde(skip_serializing_if = "Option::is_none")]
        in_reply_to: Option<usize>
    },
    #[serde(rename = "generate_ok")]
    GenerateOk {
        id: String,
        #[serde(skip_serializing_if = "Option::is_none")]
        in_reply_to: Option<usize>
    },
}

#[derive(Debug, Default)]
pub struct Node {
    // Store our ID when a client initializes us.
    node_id: Option<String>,
}

impl HandleMessage for Node {
    type Message = Message;
    type Error = std::io::Error;
    fn handle_message(
        &mut self,
        msg: Envelope<Self::Message>,
        outbound_msg_tx: std::sync::mpsc::Sender<Envelope<Self::Message>>,
    ) -> Result<(), Self::Error> {
        match msg.body {
            Message::Init { msg_id, ref node_id } => {
                self.node_id = Some(node_id.clone());
                outbound_msg_tx.send(
                    msg.reply(Message::InitOk { in_reply_to: msg_id })
                ).unwrap();
                Ok(())
            },
            Message::Generate { msg_id } => {
                outbound_msg_tx.send(
                    msg.reply(
                        Message::GenerateOk { id: uuid::Uuid::new_v4().to_string(), in_reply_to: msg_id }
                    )
                ).unwrap();
                Ok(())
            },
            _ => panic!("{}", format!("Unexpected message: {:#?}", serde_json::to_string_pretty(&msg)))
        }
    }
}

pub fn main() -> Result<(), Box<dyn std::error::Error>> {
    run(Node::default())?;
   Ok(())
}

Looking at this, you can see how much more complicated Rust's type system makes this code; it adds a fairly significant amount of syntax to grok in order to understand this code. On top of that, trying to get a useful framework to use with maelstrom was quite a hassle and getting serde to play nicely with maelstrom was a bit confusing for me.

I'm honestly not sure that I would continue playing around with this to be honest, my development experience making this felt longer than it should have for something so simple. It's one thing for this to take a while when I was trying to determine whether UUIDs are the right choice, it's a whole other thing when you know what you want and you're just trying to get the type system and the language to play the game you want them to play.

It illustrates to me why Rust is really cool but unfortunately not an ideal choice for the contexts, I tend to be in. Working with the type system that Rust has is a huge time sink when I just want to complete whatever I'm working on. To be clear, there is value in this for safety or performance critical contexts but programming for the sake of programming isn't ideal for me.

In an industrial context, my belief is that Rust asks too much in exchange for too little, the performance benefit probably isn't relevant in many cases while the maintainability benefit you get is probably outweighed by the cost incurred by the significant amount of work that has to be put in to get simple work.

That being said, I'm glad I've learned as much Rust as I have and I recommend others learn it if only because it has really cool ideas. That being said, I do still want to continue working through the Gossip Glomers. I've already gone through the first two before getting stuck on the third with Golang. I would like to eventually finish that third challenge but I am interested in exploring alternative models for concurrency.

Thinking through that, I've been investigating Elixir and the BEAM. It has some similarities with Go's concurrency model but it also has significant differences namely in that the BEAM has better abstractions for fault tolerance and that it only allows for immutable data types. On top of that, Elixir is a functional programming language which is my preferred paradigm.

So, I've been learning Elixir using Exercism, I've found it very helpful for learning the syntax by "doing" and I hope that once I've done enough problems through Exercism, I can go through Elixir In Action (and probably a Phoenix book at some point) to grasp the specifics. Eventually, once I'm comfortable enough with the language I'll give the Gossip Glomers another go this time using Phoenix. Due to the lack of any libraries that work with Maelstrom on the Elixir side I'll need to build my own way to integrate with Maelstrom from the BEAM which will be interesting.