2025-01-21 08:00:00
Rust’s focus on expressions is an underrated aspect of the language. Code feels more natural to compose once you embrace expressions as a core mechanic in Rust. I would go as far as to say that expressions shaped the way I think about control flow in general.
“Everything is an expression” is a bit of an exaggeration, but it’s a useful mental model while you internalize the concept.[1]
But what’s so special about them?
The difference between expressions and statements can easily be dismissed as a minor detail. Underneath the surface, though, the fact that expressions return values has a profound impact on the ergonomics of a language.
In Rust, most things produce a value: literals, variables, function calls, blocks, and control flow statements like if
, match
, and loop
.
Even &
and *
are expressions in Rust.
Rust inherits expressions from its functional roots in the ML family of languages; they are not so common in other languages. Go, C++, Java, and TypeScript have them, but they pale in comparison to Rust.
In Go, for example, an if
statement is… well, a statement and not an expression. This has some surprising side-effects. For example, you can’t use if
statements in a ternary expression like you would in Rust:
// This is not valid Go code!
var x = if condition else ;
Instead, you’d have to write a full-blown if
statement
along with a slightly unfortunate upfront variable declaration:
var x int
if condition else
Since if
is an expression in Rust, using it in a ternary expression is perfectly normal.
let x = if condition else ;
That explains the absence of the ternary operator in Rust (i.e. there is no syntax like x = condition ? 1 : 2;
).
No special syntax is needed because if
is comparably concise.
Also note that in comparison to Go, our variable x
does not need to be mutable.
As we will see, Rust’s expressions often lead to less mutable code.
In combination with pattern matching, expressions in Rust become even more powerful:
let = if condition else ;
Here, the left side of the assignment (a, b) is a pattern that destructures the tuple returned by the if-else
expression.
What if you deal with more complex control flow?
That’s not a problem. match
is an expression, too.
It is common to assign the result of a match
expression to a variable.
let color = match duck ;
match
and if
ExpressionsLet’s say you want to return a duck’s color, but you want to return the correct color based on the year. (In the early Disney comics, the nephews were wearing different colors.)
let color = match duck ;
Neat, right? You can combine match
and if
expressions to create complex logic in a few lines of code.
Note: those if
s are called match arm guards, and they really are full-fledged if
expressions.
You can put anything in there just like in a regular if
.
break
is an expressionYou can return a value from a loop with break
:
let foo = loop ;
// foo is 1
More commonly, you’d use it like this:
let result = loop ;
// result is 20
dbg!()
returns the value of the inner expressionYou can wrap any expression with dbg!()
without changing the behavior of your code (aside from the debug output).
let x = dbg!;
So far, I showed you some fancy expression tricks, but how do you apply this in practice?
To illustrate this, imagine you have a Config
struct that reads a configuration file from a given path:
/// Configuration for the application
/// Creates a new Config with the given path
///
/// The path is resolved against the home directory if relative.
/// Validates that the path exists and has the correct extension.
Here’s how you might implement the with_config_path
method in an imperative style:
There are a few things we can improve here:
mut
is_none()
/unwrap()
It’s always a good idea to examine unwrap()
calls and find safer alternatives.
While we “only” have two unwrap()
calls here, both point at flaws in our design.
Here’s the first one:
let mut config_path;
if path.is_absolute else
We know that home
is not None
when we unwrap
it, because we checked it right above.
But what if we refactor the code? We might forget the check and introduce a bug.
This can be rewritten as:
let config_path = if path.is_absolute else ;
Or, if we introduce a custom error type:
let config_path = if path.is_absolute else ;
The other unwrap
is also unnecessary and makes the happy path harder to read.
Here is the original code:
if config_path.is_file
We can rewrite this as:
if config_path.is_file
Or we return early to avoid the nested if
:
if !config_path.is_file
let Some = config_path.extension else
if ext != "conf"
mut
sUsually, my next step is to get rid of as many mut
variables as possible.
Note how there are no more mut
keywords after our first refactoring.
This is a typical pattern in Rust: often when we get rid of an unwrap()
, we can remove a mut
as well.
Nevertheless, it is always a good idea to look for all mut
variables and see if they are really necessary.
The last expression in a block is implicitly returned
and that return
is an expression itself, so you can often get rid of explicit return
statements.
In our case that means:
return Ok;
becomes
Ok
Another simple heuristic is to hunt for returns
and semicolons in the middle of your code.
These are like “seams” in our program; stop signs, which break the natural data flow.
Almost effortlessly, removing these blockers often improves the flow; it’s like magic.
For example, the above validation code can also be written without returns:
match config_path
I like that, because we avoid one error message duplication and all conditions start on the left.
Whether you prefer that over let-else
is a matter of taste. [2]
Remember when I said “everything is an expression”? Don’t take this too far or people will stop inviting you to dinner parties.
It’s fun to know that you could use then_some
, unwrap_or_else
, and map_or
to chain expressions together, but
don’t use them to show off.
The below code is correct, but the combinators get in the way of readability. Now it feels more like a Lisp program than Rust code.
Keep your friends and colleagues in mind when writing code. Find a balance between expressiveness and readability.
If you find that your code doesn’t feel idiomatic, see if expressions can help. They tend to guide you towards more ergonomic Rust code.
Once you find the right balance, expressions are a joy to use – especially in smaller context where data flow is key. The “trifecta” of iterators, expressions, and pattern matching is the foundation of data transformations in Rust. I wrote a complementary article about iterators here.
Of course, it’s not forbidden to mix expressions and statements!
For example, I personally like to use let-else
statements when it makes my code easier to understand.
If you’re unsure about whether using an expression is worth it, seek feedback from someone less familiar with Rust.
If they look confused, you probably tried to be too clever.
Now, try to refactor some code to train that muscle.
The Rust Reference puts it like this: “Rust is primarily an expression language. This means that most forms of value-producing or effect-causing evaluation are directed by the uniform syntax category of expressions. Each kind of expression can typically nest within each other kind of expression, and rules for evaluation of expressions involve specifying both the value produced by the expression and the order in which its sub-expressions are themselves evaluated.” ↩
By the way, let-else
is not an expression, but a statement. That’s because the else
branch doesn’t produce a value. Instead, it moves the “failure” case into the body block, while allowing the “success” case to continue in the surrounding context without additional nesting. I recommend reading the RFC for more details. ↩
2025-01-15 08:00:00
Programming is an iterative process - as much as we would like to come up with the perfect solution from the start, it rarely works that way.
Good programs often start as quick prototypes. The bad ones stay prototypes, but the best ones evolve into production code.
Whether you’re writing games, CLI tools, or designing library APIs, prototyping helps tremendously in finding the best approach before committing to a design. It helps reveal the patterns behind more idiomatic code.
For all its explicitness, Rust is surprisingly ergonomic when iterating on ideas. Contrary to popular belief, it is a joy for building prototypes.
You don’t need to be a Rust expert to be productive - in fact, many of the techniques we’ll discuss specifically help you sidestep Rust’s more advanced features. If you focus on simple patterns and make use of Rust’s excellent tooling, even less experienced Rust developers can quickly bring their ideas to life.
Things you’ll learn
The common narrative goes like this:
When you start writing a program, you don’t know what you want and you change your mind pretty often. Rust pushes back when you change your mind because the type system is very strict. On top of that, getting your idea to compile takes longer than in other languages, so the feedback loop is slower.
I’ve found that developers not yet too familiar with Rust often share this preconception. These developers stumble over the strict type system and the borrow checker while trying to sketch out a solution. They believe that with Rust you’re either at 0% or 100% done (everything works and has no undefined behavior) and there’s nothing in between.
Here are some typical misbeliefs:
These are all common misconceptions and they are not true.
It turns out you can avoid all of these pitfalls and still get a lot of value from prototyping in Rust.
If you’re happy with a scripting language like Python, why bother with Rust?
That’s a fair question! After all, Python is known for its quick feedback loop and dynamic type system, and you can always rewrite the code in Rust later.
Yes, Python is a great choice for prototyping. But I’ve been a Python developer for long enough to know that I’ll very quickly grow out of the “prototype” phase -– which is when the language falls apart for me.
One thing I found particularly challenging in Python was hardening my prototype into a robust, production-ready codebase. I’ve found that the really hard bugs in Python are often type-related: deep down in your call chain, the program crashes because you just passed the wrong type to a function. Because of that, I find myself wanting to switch to something more robust as soon as my prototype starts to take shape.
The problem is that switching languages is a huge undertaking – especially mid-project. Maybe you’ll have to maintain two codebases simultaneously for a while. On top of that, Rust follows different idioms than Python, so you might have to rethink the software architecture. And to add insult to injury, you have to change build systems, testing frameworks, and deployment pipelines as well.
Wouldn’t it be nice if you could use a single language for prototyping and production?
Using a single language across your entire project lifecycle is great for productivity. Rust scales from proof-of-concept to production deployment and that eliminates costly context switches and rewrites. Rust’s strong type system catches design flaws early, but we will see how it also provides pragmatic escape hatches if needed. This means prototypes can naturally evolve into production code; even the first version is often production-ready.
But don’t take my word for it. Here’s what Discord had to say about migrating from Go to Rust:
Remarkably, we had only put very basic thought into optimization as the Rust version was written. Even with just basic optimization, Rust was able to outperform the hyper hand-tuned Go version. This is a huge testament to how easy it is to write efficient programs with Rust compared to the deep dive we had to do with Go. – From Why Discord is switching from Go to Rust
If you start with Rust, you get a lot of benefits out of the box: a robust codebase, a strong type system, and built-in linting.
All without having to change languages mid-project! It saves you the context switch between languages once you’re done with the prototype.
Python has a few good traits that we can learn from:
The goal is to get as close to that experience in Rust as possible while staying true to Rust’s core principles. Let’s make changes quick and painless and rapidly iterate on our design without painting ourselves into a corner. (And yes, there will still be a compilation step, but hopefully, a quick one.)
Even while prototyping, the type system is not going away. There are ways to make this a blessing rather than a curse.
Use simple types like i32
, String
, Vec
in the beginning.
We can always make things more complex later if we have to – the reverse is much harder.
Here’s a quick reference for common prototype-to-production type transitions:
Prototype | Production | When to switch |
---|---|---|
String |
&str |
When you need to avoid allocations or store string data with a clear lifetime |
Vec<T> |
&[T] |
When the owned vector becomes too expensive to clone or you can’t afford the heap |
Box<T> |
&T or &mut T
|
When Box becomes a bottleneck or you don’t want to deal with heap allocations |
Rc<T> |
&T |
When the reference counting overhead becomes too expensive or you need mutability |
Arc<Mutex<T>> |
&mut T |
When you can guarantee exclusive access and don’t need thread safety |
These owned types sidestep most ownership and lifetime issues, but they do it by allocating memory on the heap - just like Python or JavaScript would.
You can always refactor when you actually need the performance or tighter resource usage, but chances are you won’t.[1]
Rust is a statically, strongly typed language. It would be a deal-breaker to write out all the types all the time if it weren’t for Rust’s type inference.
You can often omit (“elide”) the types and let the compiler figure it out from the context.
let x = 42;
let y = "hello";
let z = vec!;
This is a great way to get started quickly and defer the decision about types to later. The system scales well with more complex types, so you can use this technique even in larger projects.
let x: = vec!;
let y: = vec!;
// From the context, Rust knows that `z` needs to be a `Vec<i32>`
// The `_` is a placeholder for the type that Rust will infer
let z = x.into_iter.chain.;
Here’s a more complex example which shows just how powerful Rust’s type inference can be:
use HashMap;
// Start with some nested data
let data = vec!;
// Let Rust figure out this complex transformation
// Can you tell what the type of `categorized` is?
let categorized = data
.into_iter
.flat_map
.;
// categorized is now a HashMap<&str, &str> mapping items to their categories
println!;
It’s not easy to visualize the structure of categorized
in your head, but Rust can figure it out.
You probably already know about the Rust Playground. The playground doesn’t support auto-complete, but it’s still great when you’re on the go or you’d like to share your code with others.
I find it quite useful for quickly jotting down a bunch of functions or types to test out a design idea.
unwrap
LiberallyIt’s okay to use unwrap
in the early stages of your project.
An explicit unwrap
is like a stop sign that tells you “here’s something you need to fix later.”
You can easily grep for unwrap
and replace it with proper error handling later when you polish your code.
This way, you get the best of both worlds: quick iteration cycles and a clear path to robust error handling.
There’s also a clippy lint that points out all the unwrap
s in your code.
use fs;
use PathBuf;
See all those unwraps? To more experienced Rustaceans, they stand out like a sore thumb – and that’s a good thing!
Compare that to languages like JavaScript which can throw exceptions your way at any time. It’s much harder to ensure that you handle all the edge-cases correctly. At the very least, it costs time. Time you could spend on more important things.
While prototyping with Rust, you can safely ignore error handling and focus on the happy path without losing track of improvement areas.
anyhow
to your prototypesI like to add anyhow
pretty early during the prototyping phase,
to get more fine-grained control over my error handling.
This way, I can use bail!
and with_context
to quickly add more context to my errors without losing momentum.
Later on, I can revisit each error case and see if I can handle it more gracefully.
use ;
// Here's how to use `with_context` to add more context to an error
let home = var
.with_context?;
// ...alternatively, use `bail` to return an error immediately
let Ok = var else ;
The great thing about anyhow
is that it’s a solid choice for error handling in production code as well,
so you don’t have to rewrite your error handling logic later on.
There is great IDE support for Rust.
IDEs can help you with code completion and refactoring, which keep you in the flow and help you write code faster. Autocompletion is so much better with Rust than with dynamic languages because the type system gives the IDE a lot more information to work with.
As a corollary to the previous section, be sure to use enable inlay hints (or inline type hints) in your editor. This way, you can quickly see the inferred types right inside your IDE and make sure the types match your expectations. There’s support for this in most Rust IDEs, including RustRover and Visual Studio Code.
bacon
for quick feedback cyclesRust is not a scripting language; there is a compile step!
However, for small projects, the compile times are negligible.
Unfortunately, you have to manually run cargo check
every time you make a change
or use rust-analyzer in your editor to get instant feedback.
To fill the gap, you can use external tools like bacon
which automatically recompiles and runs your code whenever you make a change.
This way, you can get almost the same experience as with a REPL in, say, Python or Ruby.
The setup is simple:
# Install bacon
# Run bacon in your project directory
And just like that, you can get some pretty compilation output alongside your code editor.
Oh, and in case you were wondering, cargo-watch
was another popular tool for
this purpose, but it’s since been deprecated.
cargo-script
is awesomeDid you know that cargo can also run scripts?
For example, put this into a file called script.rs
:
#!/usr/bin/env cargo +nightly -Zscript
Now you can make the file executable with chmod +x script.rs
and run it with ./script.rs
which it will compile and execute your code!
This allows you to quickly test out ideas without having to create a new project.
There is support for dependencies as well.
At the moment, cargo-script
is a nightly feature, but it will be released soon on stable Rust.
You can read more about it in the RFC.
You have to try really really hard to write slow code in Rust. Use that to your advantage: during the prototype phase, try to keep the code as simple as possible.
I gave a talk titled “The Four Horsemen of Bad Rust Code” where I argue that premature optimization is one of the biggest sins in Rust.
Especially experienced developers coming from C or C++ are tempted to optimize too early.
Rust makes code perform well by default - you get memory safety at virtually zero runtime cost. When developers try to optimize too early, they often run up against the borrow checker by using complex lifetime annotations and intricate reference patterns in pursuit of better performance. This leads to harder-to-maintain code that may not actually run faster.
Resist the urge to optimize too early! You will thank yourself later. [2]
println!
and dbg!
for debuggingI find that printing values is pretty handy while prototyping. It’s one less context switch to make compared to starting a debugger.
Most people use println!
for that, but dbg!
has a few advantages:
println!
; e.g. dbg!(x)
vs. println!("{x:?}")
.Where dbg!
really shines is in recursive functions or when you want to see the intermediate values during an iteration:
dbg!;
The output is nice and tidy:
<= 1 = false
n <= 1 = false
n <= 1 = false
n <= 1 = true
n 1 = 1
* factorial = 2
n * factorial = 6
n * factorial = 24
n factorial = 24
Note that you should not keep the dbg!
calls in your final code as they will also be executed in release mode.
If you’re interested, here are more details on how to use the dbg!
macro.
Quite frankly, the type system is one of the main reasons I love Rust. It feels great to express my ideas in types and see them come to life. I would encourage you to heavily lean into the type system during the prototyping phase.
In the beginning, you won’t have a good idea of the types in your system. That’s fine! Start with something and quickly sketch out solutions and gradually add constraints to model the business requirements. Don’t stop until you find a version that feels just right. You know you’ve found a good abstraction when your types “click” with the rest of the code. [3] Try to build up a vocabulary of concepts and own types which describe your system.
Wrestling with Rust’s type system might feel slower at first compared to more dynamic languages, but it often leads to fewer iterations overall. Think of it this way: in a language like Python, each iteration might be quicker since you can skip type definitions, but you’ll likely need more iterations as you discover edge cases and invariants that weren’t immediately obvious. In Rust, the type system forces you to think through these relationships up front. Although each iteration takes longer, you typically need fewer of them to arrive at a robust solution.
This is exactly what we’ll see in the following example.
Say you’re modeling course enrollments in a student system. You might start with something simple:
But then requirements come in: some courses are very popular. More students want to enroll than there are spots available, so the school decides to add a waitlist.
Easy, let’s just add another boolean flag!
The problem is that both boolean flags could be set to true
!
This design allows invalid states where a student could be both enrolled and waitlisted.
Think for a second how we could leverage Rust’s type system to make this impossible…
Here’s one attempt:
Now we have a clear distinction between an active enrollment and a waitlisted enrollment. What’s better is that we encapsulate the details of each state in the enum variants. We can never have someone on the waitlist without a position in said list.
Just think about how much more complicated this would be in a dynamic language or a language that doesn’t support tagged unions like Rust does.
In summary, iterating on your data model is the crucial part of any prototyping phase. The result of this phase is not the code, but a deeper understanding of the problem domain itself. You can harvest this knowledge to build a more robust and maintainable solution.
It turns out you can model a surprisingly large system in just a few lines of code.
So, never be afraid to play around with types and refactor your code as you go.
todo!
MacroOne of the cornerstones of prototyping is that you don’t have to have all the answers right away.
In Rust, I find myself reaching for the todo!
macro to
express that idea.
I routinely just scaffold out the functions or a module and then fill in the blanks later.
// We don't know yet how to process the data
// but we're pretty certain that we need a function
// that takes a Vec<i32> and returns an i32
// There exists a function that loads the data and returns a Vec<i32>
// How exactly it does that is not important right now
We did not do much here, but we have a clear idea of what the program should do.
Now we can go and iterate on the design.
For example, should process_data
take a reference to the data?
Should we create a struct to hold the data and the processing logic?
How about using an iterator instead of a vector?
Should we introduce a trait to support algorithms for processing the data?
These are all helpful questions that we can answer without having to worry about the details of the implementation. And yet our code is typesafe and compiles, and it is ready for refactoring.
unreachable!
for unreachable branchesOn a related note, you can use the unreachable!
macro to mark branches of your code that should never be reached.
This is a great way to document your assumptions about the code.
The result is the same as if you had used todo!
, but it’s more explicit about the fact that this branch should never be reached:
thread 'main' panicked at src/main.rs:6:18:
internal error: entered unreachable code: Witchcraft!
Note that we added a message to the unreachable!
macro to make it clear what the assumption is.
assert!
for invariantsAnother way to document your assumptions is to use the assert!
macro.
This is especially useful for invariants that should hold true at runtime.
For example, the above code could be rewritten like this:
During prototyping, this can be helpful to catch logic bugs early on without having to write a lot of tests and you can safely carry them over to your production code.
Consider using
debug_assert!
for
expensive invariant checks that should only run in test/debug builds.
Chances are, you won’t know which parts of your application should be generic in the beginning. Therefore it’s better to be conservative and use concrete types instead of generics until necessary.
So instead of writing this:
Write this:
If you need the same function for a different type, feel free to just copy and paste the function and change the type. This way, you avoid the trap of settling on the wrong kind of abstraction too early. Maybe the two functions only differ by type signature for now, but they might serve a completely different purpose. If the function is not generic from the start, it’s easier to remove the duplication later.
Only introduce generics when you see a clear pattern emerge in multiple places. I personally avoid generics up until the very last moment. I want to feel the “pain” of duplication logic before I abstract it away. In 50% of the cases, I find that the problem is not missing generics, but that there’s a better algorithm or data structure that solves the problem more elegantly.
Also avoid “fancy” generic type signatures:
Yes, this allows you to pass in a &str
or a String
, but at the cost of readability.
Just use an owned type for your first implementation:
Chances are, you won’t need the flexibility after all.
In summary, generics are powerful, but they can make the code harder to read and write. Avoid them until you have a clear idea of what you’re doing.
One major blocker for rapid prototyping is Rust’s ownership system. If the compiler constantly reminds you of borrows and lifetimes it can ruin your flow. For example, it’s cumbersome to deal with references when you’re just trying to get something to work.
// First attempt with references - compiler error!
This code doesn’t compile because the references are not valid outside of the function.
Compiling playground v0.0.1
error: missing lifetime specifier
-/lib.rs:7:26
|
7 |
A simple way around that is to avoid lifetimes altogether.
They are not necessary in the beginning.
Use owned types like String
and Vec
.
Just .clone()
wherever you need to pass data around.
// Much simpler with owned types
If you have a type that you need to move between threads (i.e. it needs to be Send
), you can use an Arc<Mutex<T>>
to get around the borrow checker.
If you’re worried about performance, remember that other languages like Python or Java do this implicitly behind your back.
use ;
use thread;
let note = new;
let note_clone = clone;
spawn;
If you feel like you have to use Arc<Mutex<T>>
too often, there might be a design issue.
For example, you might be able to avoid sharing state between threads.
main.rs
is your best friend while prototyping.
Stuff your code in there – no need for modules or complex organization yet. This makes it easy to experiment and move things around.
Once you have a better feel for your code’s structure, Rust’s mod
keyword becomes a handy tool for sketching out potential organization. You can nest modules right in your main file.
This inline module structure lets you quickly test different organizational patterns. You can easily move code between scopes with cut and paste, and experiment with different APIs and naming conventions. Once a particular structure feels right, you can move modules into their own files.
The key is to keep things simple until it calls for more complexity. Start flat, then add structure incrementally as your understanding of the problem grows.
See also Matklad’s article on large Rust workspaces.
Allow yourself to ignore some of the best practices for production code for a while.
It’s possible, but you need to switch off your inner critic who always wants to write perfect code from the beginning. Rust enables you to comfortably defer perfection. You can make the rough edges obvious so that you can sort them out later. Don’t let perfect be the enemy of good.
One of the biggest mistakes I observe is an engineer’s perfectionist instinct to jump on minor details which don’t have a broad enough impact to warrant the effort. It’s better to have a working prototype with a few rough edges than a perfect implementation of a small part of the system.
Remember: you are exploring! Use a coarse brush to paint the landscape first. Try to get into a flow state where you can quickly iterate. Don’t get distracted by the details too early. During this phase, it’s also fine to throw away a lot failed attempts.
There’s some overlap between prototyping and “easy Rust.”
The beauty of prototyping in Rust is that your “rough drafts” have the same memory safety and performance as polished code.
Even when I liberally use unwrap()
, stick everything in main.rs
, and reach for owned types everywhere, the resulting code
is on-par with a Python prototype in reliability, but outperforms it easily.
This makes it perfect for experimenting with real-world workloads, even before investing time in proper error handling.
Let’s see how Rust stacks up against Python for prototyping:
Aspect | Python | Rust |
---|---|---|
Initial Development Speed | ✓ Very quick to write initial code ✓ No compilation step ✓ Dynamic typing speeds up prototyping ✓ File watchers available |
⚠️ Slightly slower initial development ✓ Type inference helps ✓ Tools like bacon provide quick feedback |
Standard Library | ✓ Batteries included ✓ Rich ecosystem |
❌ Smaller standard library ✓ Growing ecosystem of high-quality crates |
Transition to Production | ❌ Need extensive testing to catch type errors ❌ Bad performance might require extra work or rewrite in another language |
✓ Minimal changes needed beyond error handling ✓ Already has good performance ✓ Memory safety guaranteed |
Maintenance | ❌ Type errors surface during runtime ❌ Refactoring is risky |
✓ Compiler catches most issues ✓ Safe refactoring with type system |
Code Evolution | ❌ Hard to maintain large codebases ❌ Type issues compound |
✓ Compiler guides improvements ✓ Types help manage complexity |
Quite frankly, Rust makes for an excellent prototyping language if you embrace its strengths. Yes, the type system will make you think harder about your design up front - but that’s actually a good thing! Each iteration might take a bit longer than in Python or JavaScript, but you’ll typically need fewer iterations from prototype to production.
I’ve found that my prototypes in other languages often hit a wall where I need to switch to something more robust. With Rust, I can start simple and gradually turn that proof-of-concept into production code, all while staying in the same language and ecosystem.
If you have any more tips or tricks for prototyping in Rust, get in touch and I’ll add them to the list!
More experienced Rust developers might find themselves reaching for an impl IntoIterator<Item=T>
where &[T]
/Vec<T>
would do. Keep it simple! ↩
In the talk, I show an example where early over-optimization led to the wrong abstraction and made the code slower. The actual bottleneck was elsewhere and hard to uncover without profiling. ↩
I usually know when I found a good abstraction once I can use all of Rust’s features like expression-oriented programming and pattern matching together with my own types. ↩
2025-01-09 08:00:00
Web browsers today face increasing demands for both performance and privacy. At Brave, they’re tackling both challenges head-on with their Rust-based ad-blocking engine. This isn’t just about blocking ads – it’s about doing so with minimal performance impact while maintaining compatibility with existing filter lists and adapting to evolving web technologies.
2024-12-26 08:00:00
While we try not to get too sentimental, celebrating one year of ‘Rust in Production’ alongside the holiday season feels like a perfect occasion to reflect. For this special episode of the podcast, we’ve gathered heartfelt messages from our guests to the Rust community.
There are two common themes that run through these messages:
As we look ahead to the Rust 2024 edition, we’re excited about what’s to come. Thank you for being part of this journey with us, and here’s to a great start to 2025! May the new year bring us all faster compile times, gentler learning curves, and, if we get lucky, let-chains on stable Rust.
2024-12-13 08:00:00
A Practical Guide for Decision Makers
I wrote this guide for technical leaders and developers considering a move from TypeScript to Rust. After years of helping teams make this transition, I’ll share what works, what doesn’t, and how to make your migration successful.
TypeScript excels at making JavaScript more maintainable, but teams often hit scaling challenges as their codebases grow. You might be facing performance bottlenecks, reliability issues, or maintenance overhead. Rust offers compelling solutions to these problems, but migration needs careful planning and execution. Let me show you how to evaluate if Rust is right for your team, plan a successful migration, and keep your team productive during the transition.
In this article, you’ll learn:
Aspect | TypeScript | Rust |
---|---|---|
1.0 Release | 2014 | 2015 |
Packages | 3 million+ (npm) | 160,000+ (crates.io) |
Type System | Optional | Mandatory |
Tooling | Rich ecosystem, frequent updates | Stable, integrated toolchain |
Memory Management | Garbage collected | Ownership system, compile-time checks |
Speed | Moderate | Fast |
Error Handling | Exceptions | Explicit handling with Result
|
Learning Curve | Moderate | Steep |
TypeScript is a great language, but many teams hit a wall around the 10k to 100k lines of code mark. At this scale, the codebase becomes hard to maintain and teams start to feel the pain.
The problems are clear and specific.
While TypeScript has a type system, it remains a dynamically typed language built on JavaScript.
Types are optional and can be bypassed using any
or unknown
or by freely casting between types.
Without enough discipline, this leads to runtime errors and bugs.
Memory leaks and security vulnerabilities become more common, especially in large, long-running backend services. Some companies I’ve worked with needed regular service restarts to manage memory leaks.
External packages often have security vulnerabilities and need regular updates. Breaking changes are common, and packages frequently become unmaintained. Frameworks like Next.js introduce frequent breaking changes, forcing teams to spend time on updates instead of business logic.
Performance isn’t guaranteed. TypeScript is fast enough for most cases, but performance-critical applications will hit limitations. Error handling through exceptions and promises can be hard to reason about. Large TypeScript codebases become difficult to refactor and maintain, even with type safety.
TypeScript’s type system creates an excellent foundation for Rust adoption. Your team already understands static typing and values its benefits. This gives you a head start with Rust, which takes these concepts further and adds more powerful guarantees.
Rust enforces stronger guarantees than TypeScript through its ownership system and borrow checker. You’ll need to plan for an adjustment period. Most developers need 2-4 months to become comfortable with Rust’s ownership model. They’ll go through a phase of “fighting the borrow checker” – this is normal and temporary. Your job is to keep the team motivated during this learning curve. I’ve seen time and again that developers who push through this phase become the strongest Rust advocates. These developers then become valuable mentors for their teammates.
Rust pushes your team to understand systems concepts better than TypeScript ever required.
You need to know the difference between stack and heap allocation.
You’ll work with different string types like String
and &str
.
And you should be willing to learn what a pointer or a mutex is.
These concepts might seem intimidating at first, but they make Rust fast. Rust won’t hide these details from you. The idea is that explicit is better than implicit.
Your team will write more efficient code because Rust makes these low-level details explicit and manageable.
You don’t need to be a systems programmer to use Rust and yet, you will need to learn these concepts to become proficient in Rust.
The strict Rust compiler is your strongest ally.
You can refactor without fear because the compiler catches mistakes early and consistently.
You won’t deal with null
or undefined
errors.
Error handling becomes explicit and predictable with Result
.
NPM gives you more packages, but Rust’s ecosystem prioritizes quality. Libraries maintain strong backward compatibility. Breaking changes are rare. Rust itself releases new editions every three years with opt-in changes.
Many Rust crates stay in 0.x versions longer than you might expect. Don’t let this worry you – Rust’s type system ensures robust functionality even before reaching 1.0. The ecosystem grows fast, and the existing libraries work reliably. For specific use cases, writing your own library is common and well-supported.
TypeScript is a great language for backend services. A lot of companies use it successfully. There are many frameworks to choose from, like Express, NestJS, or Fastify. These frameworks are mature and well-documented.
By comparison, Rust’s backend ecosystem is smaller. You have Axum, Actix, and Rocket, among others. These frameworks are fast and reliable, but they don’t provide a “batteries-included” experience like Express.
That said, Rust’s ecosystem is growing fast and most companies find the available libraries sufficient. I personally recommend axum as it has the largest momentum and is backed by the Tokio team.
Deployment is straightforward. You can build your Rust service into a single binary and deploy it to a container or a server. Rust binaries are small and have no runtime dependencies. This makes deployment easy and reliable.
Teams are often shocks that Rust is so fast. They go in expecting Rust to be fast, but the reality still surprises them. You can expect an order of magnitude better CPU and memory usage if you’re coming from JS/TS. The effects of that are very real: reduced cloud costs, less hardware, and faster response times.
Most importantly, your runtime behavior becomes predictable. Production incidents decrease. Your operations team will thank you for the reduced overhead.
Write down why you want to migrate before you start:
This clarity helps when things get tough.
I know this evaluation isn’t easy. We often struggle to see our codebase’s problems clearly. Politics and inertia hold us back. Sometimes you need an outside perspective. I can help you evaluate your situation objectively and create a solid migration plan. This might sound expensive, but think about your team’s salaries and the cost of making the wrong decision. Good consulting pays for itself quickly. Reach out for a free consultation.
You have two main ways to integrate Rust with TypeScript.
You can use WebAssembly (WASM) to compile your Rust code to a library and call it directly from TypeScript. This works great for speeding up performance-critical components. Teams often start here and expand their Rust usage as they see the benefits.
Alternatively, you can deploy Rust as separate services. This fits well with microservice architectures. Your TypeScript and Rust components communicate over the network. This gives you a clean separation and lets you migrate gradually.
You need a Rust champion in your team. This person should have some prior Rust experience and be excited about the language.
Outside help can get you started, but keep the knowledge in-house. You know your codebase and business domain best. A consultant helps with the tricky Rust parts, team augmentation, and training, but your team maintains and extends the codebase in the long run.
They need to believe in the mission.
In order to succeed, your Rust champion needs to be able to motivate the team, answer questions, and guide the team through the learning curve. They work hand-in-hand with the consultant to ensure the team’s success.
Don’t rewrite everything at once! Start small. Maybe pick a monitoring service or CLI tool – something important but not critical. Perhaps you’ll give it a shot during a hackathon or a sprint. Build confidence through early wins.
Ready to make the switch to Rust?
I help teams make successful transitions from TypeScript to Rust. Whether you need training, architecture guidance, or migration planning, let’s talk about your needs.
2024-12-13 08:00:00
A Practical Guide for Decision Makers
This guide is written for technical leaders and developers considering moving their teams from Python to Rust. I used to be a Python developer myself, and I know how daunting it can be to switch to a new language. Base on years of experience helping teams make this transition, I’ll share practical insights on what works, what doesn’t, and how to ensure a successful migration.
Python is an incredibly versatile language that powers everything from web applications to data science pipelines. However, as organizations scale, they often encounter limitations around performance, type safety, and robustness. While Rust isn’t a direct replacement for Python, it has some answers to these challenges.
But is Rust the right choice for your team?
In this article, you’ll learn:
I help teams migrate from Python to Rust, providing tailored guidance and training. If you’re considering a migration, answer a few questions about your project, and I’ll reach out with a customized plan.
I'll send you a customized migration strategy based on your responses.
In the meantime, feel free to check out my other articles about Rust.
Aspect | Python | Rust |
---|---|---|
Type System | Dynamic, optional type hints | Static, strong type system |
Memory Management | Garbage collected | No GC, ownership and borrowing |
Performance | Moderate | High performance, low-level control |
Deployment | Runtime required | Single binary, minimal runtime |
Package Manager | Multiple (pip, conda, uv) | cargo (built-in, consistent) |
Error Handling | Exceptions | Result type |
Concurrency | Limited by GIL | zero-cost abstractions, no GIL |
Learning Curve | Gentle | Steep |
Ecosystem Size | Vast (500,000+ packages) | Growing (160,000+ crates) |
Python developers often want to transition to Rust for several reasons:
Developers interested in Rust are likely willing to understand systems programming concepts. They grew out of Python’s limitations and are looking for more control over performance and memory management.
Python developers often long for stronger type guarantees. They appreciate Rust’s static type system and the safety it provides.
Developers with a Python background often work on data processing or web applications. These are areas where Rust’s performance benefits are most pronounced.
While Python excels at readability and rapid development, teams often hit scaling challenges as their applications grow. Below, I’ve listed a few common pain points that get mentioned in consultations with teams considering a migration.
Python’s Global Interpreter Lock (GIL) limits true parallelism, making it challenging to fully utilize modern multi-core processors. There is a version of Python without the GIL, but it doesn’t solve the performance issues.
While tools like asyncio help with I/O-bound tasks, CPU-intensive operations remain constrained. Teams often resort to complex workarounds involving multiple processes or C extensions.
Despite Python’s type hints, runtime type errors still occur. Types are optional in Python. Developers need the discipline to add and maintain type hints consistently, which can be challenging in large codebases. Furthermore, adoption is inconsistent across the Python ecosyste (e.g., third-party libraries). Large Python applications can become difficult to maintain and refactor confidently.
From experience, there is a breaking point around the 10-100k lines of code mark where the lack of type safety becomes a significant liability.[1]
Python applications require managing runtime environments, dependencies, and potential version conflicts. A lot of the issues can be mitigated with containerization. However, bundling Python applications for deployment is not an easy task, especially when targeting platforms with different architectures.
Python has a relatively manageable memory profile, but it can be inefficient for certain workloads. For example, Python’s memory overhead can be significant for large-scale data processing or long-running services. CPU-bound tasks often get offloaded to C extensions or to worker processes, adding complexity and overhead.
The transition from Python to Rust presents unique challenges:
Python is very syntax-light, which makes it easy to read and write. By comparison, Rust is full of “symbols” and keywords that can be intimidating at first.
Developers need to see through the syntax and understand the underlying semantics to become productive. This is a critical step in the transition process.
Here is an example of a simple list comprehension in Python:
# A list of bands
=
# A list comprehension to filter for bands that start with "M"
=
# A list comprehension to uppercase the bands
=
# We get ["METALLICA", "MEGADETH"]
Rust does not have a direct equivalent to list comprehensions. Instead, iterator patterns are used to achieve the same result:
let bands = vec!;
let uppercased: = bands.iter
.filter
.map
.collect;
// uppercased = vec!["METALLICA", "MEGADETH"]
Python developers need to adjust to start “thinking in Rust” to avoid common pitfalls. Plan for a 3-4 month learning period where developers will need to understand concepts like:
These are fundamental concepts in Rust and it’s important to get them right to become really effective.
Lifetimes are another concept that can be challenging to grasp initially, but you can get a long way without fully understanding them. Don’t worry about lifetimes when you’re just starting out.
Pointer handling and boxing is another area where Python developers need to adjust. However, beginners can often get by without understanding this in detail.
Moving from Python’s dynamic typing to Rust’s static typing requires another mindset shift. Developers suddenly need to:
Option<T>
and Result<T, E>
for error handlingThis can feel intimidating at first, but it’s actually a lot of fun once you get the hang of it. The compiler is helpfully guiding you along the way, and you can start without a deep understanding of all the concepts.
The pattern matching syntax in Rust can be a game-changer for developers coming from Python and is often cited as one of the most enjoyable features of Rust.
There are several ways to integrate Rust into your Python codebase:
PyO3 lets you write Python extensions in Rust or call Rust functions from Python.
This is ideal for:
For distributed systems, you can:
If you already have a microservices architecture, this can be a great way to start. You can build new services in Rust and gradually replace old Python services as needed. The new services can be deployed alongside the old ones, and you can ensure that the APIs are compatible.
If you’re just staring out, I recommend to write a command-line tool, which is an excellent candidate for getting your feet wet.
They have all the positive indicators for a successful first project:
Let’s say you have a web application that needs to do some heavy lifting. One common way is to offload the work to a worker queue. That’s a great place to test out Rust, as you can directly compare the performance and developer experience with Python.
You can use a message queue like RabbitMQ or Kafka to communicate between the Python web application and the Rust worker.
Are you using Python for data science or ETL tasks? For example, you might be using Pandas or Dask for data processing.
Rust has some excellent libraries for data processing, like Polars and Apache Arrow. A lot of teams start by moving the data preprocessing and ETL tasks to Rust, and they like it so much that they move more and more of the business logic over.
That worked extremely well for a few clients I worked with, as they could leverage Rust’s performance and reliability for the most critical parts of their data processing pipeline. After a short learning period, the team was as productive in Rust as they were in Python.
A successful migration requires careful planning:
Start Small
Invest in Training
Measure Success
Rust’s ecosystem is smaller than Python’s.
On top of that, Rust has a much smaller standard library compared to Python. This means you’ll commonly rely on third-party crates for functionality that’s built into Python.
Depending on your use case, here are some key differences to consider:
Python dominates data science with libraries like NumPy and Pandas. However, Rust is making inroads with libraries like Polars and Apache’s Arrow.
You don’t have to migrate your entire data science stack to Rust. Chances are, you have experts on your team who are comfortable with doing data analysis in Python. You can start by using the Rust libraries for data preprocessing and ETL tasks. They have Python bindings, so you get a lot of the benefits of Rust without having to do a full migration.
Rust wasn’t initially planned to be a strong contender in the web development space. This has changed in recent years with the rise of frameworks like Axum and Loco. Now, Rust is a viable option for building high-performance APIs and web applications. It is one key are the Rust team is investing in, and the ecosystem is maturing rapidly.
In combination with sqlx for database access and serde for serialization, Rust is a very effective choice for web backends. What surprises many Python developers is how similar it is to working with other web frameworks like Flask or FastAPI. Another surprise is how robust the final product is, as it catches many bugs at compile time and scales extremely well. Production Rust web applications are extremely robust. This lifts a lot of the burden from the operations team. I expect more backend services to be written in Rust in the future – especially for high-performance applications.
Not everything needs to be migrated! Python excels at:
Consider a hybrid approach where each language handles what it does best.
One winning strategy is to explore a space in Python and moving production-workloads to Rust once the requirements are clear and the project can benefit from performance and scale. Speaking of which…
Actual performance numbers vary significantly based on workload and implementation, so take these numbers with a grain of salt.
Based on my experience helping teams migrate, here are typical improvements:
Especially the P90 latency improvements are often surprising to teams. There are very few outliers and things tend to run smoothly, no matter the load. In Python, this is a common source of frustration, where a single request can take significantly longer than the rest. Production payloads are extremely boring in Rust. The DevOps team will thank you.
Migrating to a different programming language is a significant undertaking that requires careful planning and execution. The step from Python to Rust is no exception.
While the learning curve is steep, the benefits in terms of performance, reliability, and maintainability can be substantial.
Success depends on:
Remember that this isn’t an all-or-nothing decision! Many organizations successfully use Python and Rust together, leveraging each language’s strengths. Write down your reasoning for the migration. Many issues can be solved in Python, and the migration might not be necessary. However, if you’re hitting the limits of Python, Rust is a strong replacement.
Ready to Make the Move to Rust?
I help teams make successful transitions from Python to Rust. My clients moved critical workloads to Rust and saw significant improvements in performance and reliability. Whether you need training, architecture guidance, or migration planning, let’s talk about your needs.