← Back

Thinking in Rust: the concepts that do not come from other languages

A reflection on the core concepts of Rust that change the mental model of the developer: ownership, borrowing, lifetimes, errors, and software design.

Rust
Thinking in Rust

Learning a new programming language costs more or less depending on the experience you already have with others. Some are learned by accumulation: you add syntax, libraries, tricks, and practice until exhaustion.

Then there is Rust, which is learned by substitution. Not because it is whimsical, but because it forces you to change the mental model you have used for years to solve problems, with mechanisms in the language that, I admit, took me a long time to digest until at some point something clicked and I understood the reason behind these points that at first seem a bit esoteric, especially if you make the mistake of comparing them to other languages.

If you come from interpreted, high-level languages such as Python or JavaScript, Rust will sound like Chinese to you, literally.

If your world is Java or C#, the learning curve will be medium.

And finally, if you know C++, learning Rust will feel natural, although it will still require some effort.

At the beginning of my professional career (more years ago than I care to remember), I started programming in C++, so when I approached Rust a few years ago, I thought it would be pleasant and even attractive. But it was not. I began by reading the classic book Programming Rust and all the language documentation (which is very good, by the way), but I got stuck several times, after trying a project in WebAssembly with Rust (which ultimately did not happen).

Nevertheless, I kept coming back to Rust and its ecosystem, mainly out of curiosity and because of scalability in my HPC infrastructure experiments (high performance computing), where saving every last byte and CPU cycle matters.

And that is where this article comes from: its main intention is to insist on the concepts you must assume and understand very well from the start of Rust, so you do not crash until you begin to see the whole forest.

Nowadays I feel some fluency with Rust; in fact, I would even say enthusiasm. Rust does not ask, “how do you want to program?” It asks, “what guarantees are you willing to declare?” And in that question, which has a certain philosophical tone, lies the reason it is so difficult at first… and the reason that, when you finally see the light, you do not look at the language or its ecosystem the same way again.

I have always hated childish comparisons between languages and environments: each one has its place and niche, and our skill is to know which environments, technologies, and so on are the most appropriate for each project (and its future lifecycle).

This article, in the form of a compass, is a way of naming what usually hurts in Rust when you come from other languages and what I would have liked to find at the beginning.

In a way, Rust is not a new syntax, but a kind of new pact when programming, very different from other languages, although it obviously has similar connotations to C++.

1) Rust is not C++, not Java, not Python (even if it has some similarities with specific elements of all of them)

It is tempting to classify Rust by proximity or similarity to other languages:

  • “It looks like C++ because it compiles and is fast.”
  • “It looks like Java/C# because it has traits reminiscent of interfaces.”
  • “It looks like Python in its modern ergonomics and tooling.”

And yet, if you approach it from those comparisons, Rust remains a mock-up to you: it looks similar on the outside, but inside it does not fit.

In C++, power is negotiated with conventions, discipline (don’t forget the delete), and a culture of “do the right thing,” but nothing, except your own good judgment, forces you to do the right thing. In Rust, as we will see, this is different.

In Java/C#, memory and object lifetimes are externalized to a runtime and to a powerful garbage collector that makes programming easier and frees you from thinking about the lifecycle of variables and resources. In Python, the cost is in execution, not compilation; you focus on functionality, and the language gives you certain guarantees, but everything has a cost, of course.

Rust, on the other hand, slaps you in the face by moving a significant part of an application’s complexity to the compile time. Yes, you read that right: it took me a long time to grasp this idea and its reason.

In other languages, many internal, low-level aspects of an application are handed to you ready-made, but Rust is much more explicit, and this hurts at first (and later you fall in love precisely with this quality because it becomes natural and productive to work with).

2) Memory usage is not an internal detail: it is part of the design

The main purpose of Rust is to prevent bugs before they appear at runtime: memory leaks, data races, dangling pointers, forbidden accesses, and so on. To do that, for many types of errors, you need to think explicitly about how your application uses memory internally. But don’t worry: in reality, this is easier than it sounds.

In many languages, memory management does not appear anywhere: you declare a variable or instantiate an object, use it, and then forget about it; a garbage collector does the work. But to gain that fluidity while writing code, you have to pay a price: either in inefficient memory usage and poor optimization, or in the introduction of errors that will eventually surface.

In Rust, memory becomes part of the architecture. This took me a long time to understand (after being deeply accustomed to C# and JavaScript with Node).

In reality, this is an advantage… if you accept something shocking at the beginning:

  • There is no garbage collector to cushion the cost of vague decisions.
  • The lifetime of data is not an accident: it is a contract.

That is, you have to explicitly indicate what you are going to use a variable or resource for, how it will be used, and what its lifecycle will be in the application.

This is the first major friction: in Rust, the language asks, “who owns this?” and it does so because it wants to guarantee something very concrete: that release is safe, deterministic, and efficient, and that it happens as early as possible to free resources. This is achieved directly by the nature of the language itself.

To do this, it introduces the concept of ownership, move, and borrow of variables and resources:

  • Ownership: each value has a unique owner.
  • When the owner goes out of scope, the value is freed and that is it.
  • The value can change owner (called a “move”).
  • When a value is borrowed, ownership is not transferred.

And that is it: simple ideas, but with powerful impact.

The following snippet shows all of this (and believe me, at first it is very frustrating to see how, for just a few lines of code, the compiler keeps complaining until you fix what is wrong):

fn main() {
    let s = String::from("El Libro Negro del Programador");
    let t = s;
    println!("{}", t);
    println!("{}", s); // error: s no longer owns the value
}

But the important thing is that this error is detected at compile time, not at runtime, and that has an extraordinary impact on application safety and efficiency.

When the Rust compiler says everything is fine, it guarantees that the code is formally correct, at least from the perspective of data races, incorrect value assignments, and a thousand other things.

3) Mutability and aliasing: when &mut is a promise of exclusivity

If you come from languages where you can have multiple references and mutate almost anywhere, Rust will seem strange and even hostile. But the key rule is elegant once you understand it:

  • You can have many immutable references (&T) to the same value.
  • There can be only one mutable reference to a value (&mut T).

For those who know C or C++, & will sound familiar, but in Rust there is no concept of a “pointer”; it is simply indicating a reference to a value and the right to read or change it.

This seems like an obstacle until you look at it for what it really is: a design principle to eliminate data races and illegal intermediate states that can cause catastrophic errors when the application is running.

The important thing is that Rust forces you to declare when something can change, and the natural consequence is that this helps not only the compiler, but also the reader of the code to understand it, and also the design of the application.

But everything has a cost: you have to think everything through beforehand. At first, as I said, it is very frustrating to see how any small piece of code triggers multiple compiler errors until you internalize the rules and start approaching code in that way naturally.

4) Types as contracts: Option and Result are not “verbosity”, they are domain

Accustomed to handling exceptions in C# and almost optionally, you arrive in Rust and discover that the language itself forces you to handle errors when a function may fail for some reason.

At first, this is very annoying, but eventually you realize that thanks to this your application, in an innate way, has fewer chances of error or introducing bugs, because it forces you to manage and think through every situation.

In Rust, errors and the absence of a value are not exceptions or nulls that appear at runtime; instead, they are handled at compile time using types like Result<V, E> and Option<T>. In Rust, by design, it is impossible to use a value that has become null.

What Rust is forcing you to do is express the possibility that something may fail and handle it in the code.

The discipline here is not aesthetic. It is operational and inherent to the language.

5) Traits and generics: it is not “pretty” polymorphism, it is structural design

If ownership makes you sweat, traits usually come afterward with their own cost, especially when bounds, lifetimes, and associated types appear.

The key mental leap is this:

  • In many languages: “I inherit to reuse.”
  • In Rust: “I express capabilities to compose.”

Another subtle point to understand is its impact on application design.

Traits are capabilities, contracts of behavior, and Rust lets you choose the kind of polymorphism you need:

  • Static (generics): more performance, monomorphization.
  • Dynamic (dyn Trait): flexibility.

Up to here it is simple. What is difficult is understanding that design in Rust is based less on hierarchies and more on the composition of capabilities.

When you accept this, you stop fighting the language and begin to use it as an architecture tool.

6) The compiler does not only compile; it is a lightweight formal verifier

Rust has a special relationship with the compiler. In other languages, compiling is something like a formality check: verifying that the syntax is correct. In Rust, compiling is a conversation, sometimes frustrating until you begin to master it.

Compiler messages are usually long, specific, and I would even say pedagogical. And that is intentional: Rust tries to help you maintain invariants that, otherwise, would escape you at runtime and likely cause errors.

But here is a truth worth accepting early: if the compiler complains, it is usually not because you forgot a comma (though that can happen), but because you have not expressed an idea correctly.

When the borrow checker does not let you move forward, it is not signaling a syntax error: it is signaling ambiguity in the lifetime or access to the data, which is a likely source of runtime errors. Something that would work in other languages until it stopped working.

Rust forces you to resolve that ambiguity as early as possible.

7) Concurrency without data races: the “superpower” that has a price

Rust has an implicit claim: if it compiles, certain classes of data race cannot exist by design of the language. This does not make you invincible, but it gives you an incredible foundation.

To give a couple of examples, here are two key concepts:

  • Send and Sync (automatic traits) to transfer and share safely across threads.
  • Ownership and borrowing so concurrency is not a festival of mutable aliasing.

The result is extraordinary: Rust does not “fix” concurrency, but it narrows the space of possible errors enormously.

Again: more work at the beginning, fewer surprises later.

In conclusion, Rust demands more effort at compile time and forces you to answer these questions in code:

  • Who owns this data?
  • How long will it live?
  • Who can mutate it?
  • What happens if it fails?
  • What guarantees does this type provide?
  • What capabilities does this type have and which ones do I need to use?

At first this feels like annoying bureaucracy, but later you discover it was pure design to produce more efficient and safer code. Over time, it becomes a way of programming that does not depend as much on human memory, but on verifiable invariants that the language imposes: a way of thinking in a set of simple, almost axiomatic rules, but with extraordinary impact.

Rust does not make you write more out of whim. It forces you to declare what you used to leave implicit (and much more prone to bugs and inefficient memory usage), and that forces you to be more precise, more disciplined, and more aware. And precisely because of that, once you get used to it, the feeling changes: it is not that Rust slows you down; it is that it makes you more reliable.

And yes, it hurts at first. But then you realize that the feeling of “it is preventing me from programming” is exactly the same as when a good architect tells you, “no, that solution does not scale,” or a good coach forces you to look beyond the first idea. Rust is not stopping you: it is teaching you to think better before you write.