Everyone says Rust will break your brain. So when I finally opened a real Rust project — not a tutorial, real code with real bugs — I got ready for the pain. Then I read it, and it was just match, and Option, and trait, and <T>, and functions that send errors back up with Result instead of throwing them. I’d seen all of this before. I relaxed too soon.
Then a & showed up in front of a parameter. A function had a tiny 'a in it that I was sure was a typo. Someone called .clone() where I couldn’t see why you’d copy anything — I later found out that’s often just Rust’s “fine, make it compile” button. And I hit a question Scala never asked me: who owns this memory, and for how long? That’s what this series is about. The rest is mostly new names for things you already know.
This post has two parts. First the good news, which is most of it: a lot of what you know from Scala works the same way, just with new names, and by the end you’ll be able to read Rust. Then the part that’s actually new, where the rest of the series lives. I’ll assume you know Scala or Java, so I won’t stop to explain pattern matching or Option. New names first, then the one thing that’s really new.
Is Rust hard if you already know Scala?
Short version: not really. There’s exactly one genuinely hard idea — ownership — and the rest is renaming. So let’s start with everything that already transfers.
Most of it is just Scala with new names
Here’s a quick test. The same small piece of code, written twice — once in Scala, once in Rust. Don’t read them as two languages. Put them side by side and see how little actually changes.
// Scala — sealed trait + case classessealed trait Shapecase class Circle(radius: Double) extends Shapecase class Rectangle(width: Double, height: Double) extends Shape
def area(s: Shape): Double = s match { case Circle(r) => math.Pi * r * r case Rectangle(w, h) => w * h}// Rust — enum + matchenum Shape { Circle { radius: f64 }, Rectangle { width: f64, height: f64 },}
fn area(s: Shape) -> f64 { match s { Shape::Circle { radius } => std::f64::consts::PI * radius * radius, Shape::Rectangle { width, height } => width * height, }}A sum type, pattern matching, an exhaustive match — all things you already use, all where you’d expect them. Rust’s enum is your sealed trait and its case classes, just rolled into one keyword. And like in Scala, the match has to cover every case, or the compiler complains. If you’re on Scala 3 it’s even closer, since Scala 3 has enum too. You’ve basically written this already.
It’s like this for most things, not just enums. Most of what you use every day has a close match in Rust. Here’s a rough table:
| Concept | Scala | Rust |
|---|---|---|
| Immutable / mutable binding | val / var | let / let mut |
| Sum type (ADT) | sealed trait + case class | enum with variants |
| Pattern matching | match (exhaustive) | match (exhaustive) |
| Optionality (no null) | Option[T] | Option<T> |
| Fallible result | Either[E, A] / Try[A] | Result<T, E> |
| Interface-like abstraction | trait | trait |
| Generics | [T] | <T> |
| Type bounds | T <: X / context bound | T: X (multiple with +) |
| Closures / lambdas | x => ... | |x| ... |
Things that look the same but aren’t
Some things look like Scala but work differently underneath. Here are four that caught me out early, while I still thought I had it figured out:
structis not a class. It only holds data, no methods inside. The methods go in a separateimplblock. I still can’t decide if that’s cleaner or just more typing.traitis not a base class. Noextendschain, no data inside. It’s just behaviour you add to a type. That’s fine most of the time, and a bit annoying on the day you actually wanted inheritance.- Type inference stops at the function edge. Rust infers types inside a function, but you have to write the full signature, return type included. It’s a bit more typing. The upside is you never have to guess what a function returns, which in Scala happens a lot.
- Closures pick how they capture. A Rust closure looks just like your lambda, but it asks something the JVM never does: does it borrow the values it uses, or own them? With a garbage collector that choice doesn’t exist. It’s usually the first place the new stuff sneaks into something you thought you knew.
Then it starts being Rust
That last point is the door to the new part. Look at the area function from before, the one that takes the shape by value. It compiles and runs, but it’s a little wrong in a way no Scala reviewer would catch: taking a Shape by value uses it up. After area(s), the shape s is gone. You used up the shape just to measure it. In Rust, the fix is one character.
fn area(s: Shape) -> f64 // what we wrote (by value — it consumes the shape)fn area(s: &Shape) -> f64 // what a Rust dev would actually writeThese two are almost the same Scala you read earlier. The only real difference is that one &. It means “borrow the shape” instead of “take it.” A Rust dev adds it without thinking, and the reason why is basically this whole series.
And if the & only surprised you a little, this is where the syntax gets really weird:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y }}That little apostrophe, 'a, is a lifetime. The first time you see one it looks like a typo the compiler somehow accepted. It’s not pretty, and I won’t pretend it is. I’m not going to explain it here either — it gets its own post. I just wanted to show it, because the title said “until it starts being Rust,” and this is the moment it does.
So the new part has a name: ownership, borrowing, and lifetimes — what & and 'a both point to. It all comes from one fact: Rust has no garbage collector. Nobody cleans up your memory at runtime. That job has to go somewhere, so Rust gives it to the compiler. The compiler has to work out, before your program even runs, who owns each piece of memory and how long each borrow lasts. The &, the .clone(), the apostrophes all come from that one extra job. If you take one thing from this series, take this: in Scala the compiler checks your types, and in Rust it checks your types and your memory too.
None of this is free, and I’ll say so plainly. You pay with a compiler that argues with you, slow builds, and the odd afternoon spent fighting the borrow checker over code you know is fine. In return you get no GC pauses, and a whole group of memory bugs that just can’t compile. Whether that’s worth it depends on what you’re building. I don’t think it’s always worth it, and I’d be a bit wary of anyone who says it is.
You can feel the same thing in something as small as usize, the type Rust uses for indexes and sizes. It’s unsigned, so “this can’t be negative” isn’t a comment or a runtime check, it’s part of the type. The JVM just hands you an int and trusts you. It’s a small thing, but it’s the same idea: Rust likes to check things up front instead of trusting them.
So where does that leave you? One half of Rust is the half you just read easily — match, enum, Option, ?. That half is already yours, so you can stop bracing for it. It really does keep working just like Scala. The other half is ownership. I wouldn’t call it a tax on the easy half. It’s closer to the reason Rust exists in the first place. It’s the hard part, and it’s also why you’d reach for Rust at all.
That & we added on one line turns out to matter a lot. It’s where Part 2 starts, where we finally answer the question this post kept circling: who owns this, and for how long? Keep the area function handy — we’re not done with it yet.