Introduction: Two Worlds in One Language
\n\nWhen 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\nMany 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\nThe 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\nOOP Basics in Rust: Traits Instead of Classes
\n\nLet'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\nStructs and Methods
\n\nStructs are your "objects." You define the data, and then use impl to add methods to them:
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\nThis is pure OOP style: we have state (struct fields) and behavior (methods). Rust ensures encapsulation through visibility control (the pub modifier).
Polymorphism through Traits
\n\nInstead 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\ntrait 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\nNow you can work with any object that implements the Attackable trait without worrying about its concrete type:
fn perform_attack(attacker: &impl Attackable, target: &mut Player) {\n attacker.attack(target);\n}\n\n\nThis is called "duck typing on steroids" or static polymorphism. Rust checks everything at compile time, so you won't get a runtime error.
\n\nFunctional Approach: Iterators and Closures
\n\nNow let's switch