Rust: Functional or OOP? Choosing a Style for Ideal Code

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: Two Worlds in One Language

\n\n

When you start learning Rust, you quickly notice its unique position. This language doesn't force a single way of thinking on you. Unlike Java (where everything is an object) or Haskell (where everything is a function), Rust offers you the freedom of choice. But with this freedom comes responsibility: which approach to choose — functional or object-oriented?

\n\n

Many developers coming to Rust from the world of Python or C++ intuitively gravitate towards OOP (object-oriented programming). They look for classes, inheritance, and familiar patterns. Others, inspired by the world of Haskell or Scala, try to write in Rust exclusively in a functional style, using monads, immutability, and composition.

\n\n

The truth, as always, lies somewhere in the middle. Rust is a multi-paradigm language that allows you to take the best from both worlds. In this article, we will break down how to effectively use the functional approach in Rust and when to apply object-oriented patterns. You will learn how to write code that not only works but also delights with its elegance.

\n\n

OOP Basics in Rust: Traits Instead of Classes

\n\n

Let's set the record straight right away. Rust does not have classes in the classical sense. But that doesn't mean you can't use OOP. Instead of classes, traits and structs are used here. It is they that allow you to implement encapsulation, polymorphism, and even a semblance of inheritance.

\n\n

Structs and Methods

\n\n

Structs are your "objects." You define the data, and then use impl to add methods to them:

\n\n
struct Player {\n    name: String,\n    health: i32,\n    level: u32,\n}\n\nimpl Player {\n    // Constructor method\n    fn new(name: &str) -> Self {\n        Player {\n            name: name.to_string(),\n            health: 100,\n            level: 1,\n        }\n    }\n\n    // Method that changes state\n    fn take_damage(&mut self, damage: i32) {\n        self.health = (self.health - damage).max(0);\n    }\n\n    // Getter (accessor method)\n    fn is_alive(&self) -> bool {\n        self.health > 0\n    }\n}\n
\n\n

This is pure OOP style: we have state (struct fields) and behavior (methods). Rust ensures encapsulation through visibility control (the pub modifier).

\n\n

Polymorphism through Traits

\n\n

Instead of class inheritance, Rust uses polymorphism through traits. This is more powerful and safer than classical inheritance, as you don't encounter the "diamond problem" or accidentally inherit unwanted behavior.

\n\n
trait Attackable {\n    fn attack(&self, target: &mut Player);\n}\n\nstruct Swordsman {\n    damage: i32,\n}\n\nimpl Attackable for Swordsman {\n    fn attack(&self, target: &mut Player) {\n        target.take_damage(self.damage);\n        println!("The swordsman dealt {} damage!", self.damage);\n    }\n}\n\nstruct Mage {\n    spell_power: i32,\n    mana_cost: i32,\n}\n\nimpl Attackable for Mage {\n    fn attack(&self, target: &mut Player) {\n        target.take_damage(self.spell_power * 2);\n        println!("The mage cast a fireball with power {}!", self.spell_power * 2);\n    }\n}\n
\n\n

Now you can work with any object that implements the Attackable trait without worrying about its concrete type:

\n\n
fn perform_attack(attacker: &impl Attackable, target: &mut Player) {\n    attacker.attack(target);\n}\n
\n\n

This is called "duck typing on steroids" or static polymorphism. Rust checks everything at compile time, so you won't get a runtime error.

\n\n

Functional Approach: Iterators and Closures

\n\n

Now let's switch

Blogs

Book Recommendations