Hey there! How’s it going? How’s the start of the year treating you so far? Today, I’m here to talk about one of the small promises I made to myself at the beginning of the year: learning Rust.
I wanted to write this short post to share my first impressions and experiences with Rust as someone who, basically, has zero knowledge of memory management. My background is in functional programming, mainly in languages like Scala and some Haskell, so this is quite a shift for me.
First Impressions #
To put it briefly: Rust doesn’t seem like a difficult language—at least not as much as people make it out to be. However, it does introduce some mechanics that we’re not used to, and if you have a background similar to mine, it’s also a paradigm shift.
At times, I’ve felt like I’m re-learning concepts that I had deliberately trained myself to forget—specifically, side effects and mutability. (I’ll dive into this in more detail soon.)
Rust is not Scala. Rust is Rust #
This is one of the hardest things for me to wrap my head around. Through repetition and frustration, I’ve had to force myself to accept that I’m NOT coding in Scala.
What does this mean? Well, not only do I lose conveniences like for-comprehensions and syntactic sugar, but without a GC, I also can’t do things like this and expect everything to be fine:
fn do_something_string(text: String) -> String {
// whatever
todo!()
}
fn main() {
let s1 = String::from("Hello, Rust!");
let s2 = do_something_string(s1); // `s1` is moved to the function and returned
println!("{}", s2); // âś… It Works
// println!("{}", s1); // ❌ ERROR: `s1` no longer exists (moved)
}
Why? Because in Rust, when you pass a variable that doesn’t implement Copy, ownership is transferred instead of being copied. If I want to keep using s1 after calling the function, I either need to clone it before (s1.clone()) or make sure the function returns it, as in this case.
Methods that Modify Instead of Returning #
Another thing that really caught me off guard is that some methods don’t return a new result but instead modify the original state. This is especially true for types stored on the heap, like vectors.
Scala
val xs: Array[Int] = Array(1,2,3,4,5)
val ys = xs.reverse
ys // res: Array(5,4,3,2,1)
Rust
let mut xs: Vec<i32> = vec!(1, 2, 3, 4, 5);
// let ys: () = xs.reverse();
xs.reverse(); // Modifies `xs` in place
println!("{:?}", xs); // [5, 4, 3, 2, 1]
Unlike in Scala, reverse() does not return a new vector—it mutates xs directly. Also, xs must be mutable for this to work.
A Paradigm Shift #
This is a programming paradigm I’m not used to. There’s a quote from Wojciech Pituła in his post Rust from a Scala Perspective: Advent of Code 2024 that I strongly relate to:
Weird mix of imperative and functional concepts — Developing in Rust felt like “let’s do FP until you can’t and switch to imperative then”.
So true. I highly recommend reading his post because he explains, much better than I could, many of the challenges I’ve encountered.
But Honestly… I Love It #
None of this is bad—it’s just different. It’s all about getting used to the new mechanics. But beyond that, I really like how Rust handles granularity and type semantics.
I like that Rust makes a clear distinction between types that go on the heap and those that go on the stack. I like that it forces me to think about which types have Copy semantics and which have Move semantics.
And one of my favorite things so far? Trait implementation.
Traits and Implementation #
This is something we see in Haskell and (fortunately or unfortunately—because it arrived late) we now have in Scala 3. I’m talking about how traits are implemented.
But it’s not just about how—it’s also about when and why. In Scala, traits like Functor can be implemented inside the ADT itself, or externally using type classes. But in Rust, just like in Haskell, traits are always defined separately.
This reinforces the idea that a trait is not an intrinsic part of the ADT, but rather an abstraction that can be applied to multiple types.
enum Rational {
Rational(i32, i32),
}
impl fmt::Display for Rational {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Rational::Rational(x, y) => write!(f, "R[{}/{}]", x, y),
}
}
}
// In order to add two rational elements by using `+` op
impl Add for Rational {
type Output = Rational;
fn add(self, rhs: Self) -> Self::Output {
match (self, rhs) {
(Rational::Rational(a1, b1), Rational::Rational(a2, b2)) => {
Rational::Rational(a1 * b2 + a2 * b1, b1 * b2)
}
}
}
}
// In order to clone by using .clone()
impl Clone for Racional {
fn clone(&self) -> Self {
match self {
Racional::Racional(x, y) => Racional::Racional(*x, *y),
}
}
}
fn main() {
let r1 = Racional::Racional(1, 2);
let r2 = Racional::Racional(1, 3);
println!("{} + {} = {}", r1.clone(), r2.clone(), r1 + r2) // R[1/2] + R[1/3] = R[5/6]
}
Conclusion: Beyond the Basics #
Obviously, I haven’t even scratched the surface of Rust. I’ve only been playing with it for a couple of weeks. I haven’t even touched on lifetime parameters, the borrow checker, or the compiler (which I still have a love-hate relationship with).
And while I think I’m starting to understand why Rust is gaining so much popularity, I’m still struggling internally with one big question: Is Rust the language for me?