Successful Object-Oriented Programming (OOP) in Rust: Mastering Five Critical Concepts From Python

Successful Object-Oriented Programming (OOP) in Rust: Mastering Five Critical Concepts From Python

Obect-oriented programming, the definition of OOP remains a contentious topic, with numerous computer languages vying for acceptance.

Rust is not one of them.

I was dishearted when I read, from several prestigious sources, that Rust was not object-oriented.

Luckily, I found the object-oriented behaviors I used in Python; I could get almost all of them in Rust. The one I could not get, inheritance, I get close enough in Rust.

I will repeat it.

Strap in and prepare for my journey so far into Rust and object-oriented programming!

OOP in Python

Python is an object-oriented programming language, which means it supports a variety of object-oriented behaviors — five of which I detail for Python and then Rust.

Classes and objects: Python

Python allows you to define classes, which are user-defined types that can have attributes and methods. Objects are instances of classes that can hold specific values of their attributes.

Here is an example of an Object-Oriented Programming (OOP) “stubby” pattern for a 3D Vector in linear coordinates implemented in Python:

import math

class Vector3d:
    """A three-dimensional vector with Cartesian coordinates."""
    def __init__(self, x, y, z):
        """Initializes the vector with the given coordinates."""
        self.x = x
        self.y = y
        self.z = z
    def __str__(self):
        """Returns a human-readable string representation of the vector."""
        return "({}, {}, {})".format(self.x, self.y, self.z)
    def __repr__(self):
        """Returns an unambiguous string representation of the vector."""
        return "Vector3d({}, {}, {})".format(self.x, self.y, self.z)

    def magnitude(self):
        """Returns the magnitude of the vector."""
        return math.sqrt(self.x ** 2 + self.y ** 2 + self.z ** 2)
    def normalize(self):
        """Returns a normalized version of the vector."""
        return self / self.magnitude()
    def angle(self, other):
        """Returns the angle between the vector and the given vector in radians."""
        return math.acos(self.dot(other) / (self.magnitude() * other.magnitude()))        
Note: The above “stubby” pattern, which is my irrelevant label, exists because a more complete solution of the Vector3d class would require introducing the OOP overloading and polymorphism behaviors too early.

Python does not require explicit type annotations or operator traits, but I do it as a matter of discipline.

I use type hints to clarify the expected types of function arguments and return values.

Somebody will write a Python compiler that uses type annotations as strong typing.

Note: Seems that a group has taken a crack at creating a compiler for Python.
from typing import Tuple

class Vector3d:
    """A three-dimensional vector."""
    def __init__(self, x: float, y: float, z: float) -> None:
        """Initializes the vector with the given coordinates."""
        self.x: float = x
        self.y: float = y
        self.z: float = z
    def __str__(self) -> str:
        """Returns a human-readable string representation of the vector."""
        return "({}, {}, {})".format(self.x, self.y, self.z)
    def __repr__(self) -> str:
        """Returns an unambiguous string representation of the vector."""
        return "Vector3d({}, {}, {})".format(self.x, self.y, self.z)

    def magnitude(self) -> float:
        """Returns the magnitude of the vector."""
        return math.sqrt(self.x ** 2 + self.y ** 2 + self.z ** 2)
    def normalize(self) -> 'Vector3D':
        """Returns a normalized version of the vector."""
        return self / self.magnitude()
    def angle(self, other: 'Vector3D') -> float:
        """Returns the angle between the vector and the given vector in radians."""
        return math.acos(self.dot(other) / (self.magnitude() * other.magnitude()))        

The __repr__ is often ignored because of the __str__ method. The __repr__method is used to specify how the vector should be represented as a string when it is displayed without using print(..).

v1 = Vector3d(3.3, 3.3, 0)
v2 =  Vector3d(3.3*10, 3.3, 5)
v3 = Vector3d(3.3, 3.3, 50)
v1
v2
v3
# b
print("Magnitude of v1:", v1.magnitude())
# do ahead, i warned you
print("Normalized v1:", v1.normalize())
print("Angle between v1 and v2:", v1.angle(v2))        

The output from evaluating the above Python statements is:

The class appears to have no attribute (method) ‘dot.’ I will fix that in the section Python: Overloading.

Inheritance: Python

Python supports inheritance, which allows you to define new classes that inherit attributes and methods from a parent class. You can create a hierarchy of classes with shared behavior.

But does inheritance benefit an aspect of the Python interpreter, or does it benefit the programmer?

Inheritance is a powerful feature of Python that is used to improve the modularity and reusability of your code.

You decide.

“Too much syntactic sugar can result in syntactic diabetes.” — Anonymous

I have never seen a hierarchy with over four tiers. Maybe you have.

Here’s an example of how you can define a PolarVector3d child class that inherits from the Vector3d parent class.

class PolarVector3d(Vector3d):
    """A three-dimensional vector with polar coordinates."""

def __init__(self, r: float, theta: float, phi: float) -> None:
        self.r: float = r
        self.theta: float = theta
        self.phi: float = phi        

I can create PolarVector3d objects like we are creating Vector3d objects. The PolarVector3d the class inherits all of the methods and attributes of the Vector3d class.

It is easy to inherit in Python, but we need to override some of the inherited methods, such as __init__,__str__,and __repl__, Also, there are some methods we should add, such as to_cartesian.

import math

class PolarVector3d(Vector3d):
    """A three-dimensional vector with polar coordinates."""
    def __init__(self, r: float, theta: float, phi: float) -> None:
        self.r: float = r
        self.theta: float = theta
        self.phi: float = phi
        x: float = r * math.sin(phi) * math.cos(theta)
        y: float = r * math.sin(phi) * math.sin(theta)
        z: float = r * math.cos(phi)
        
        super().__init__(x, y, z)
        
    def __repr__(self):
        return f"PolarVector3d(r={self.r}, theta={self.theta}, phi={self.phi})"
    def __str__(self):
        return f"PolarVector3d(r={self.r}, theta={self.theta}, phi={self.phi})"
    def to_cartesian(self):
        return Vector3d(self.x, self.y, self.z)        

The PolarVector3d the class inherits from the Vector3d class by specifying Vector3d as the parent class in the class definition.

The __init__ method of PolarVector3d takes r, phi, and theta as arguments and computes the x, y, and z coordinates of the vector in Cartesian coordinates using the standard conversion equations. It then calls the __init__ method of the parent class used super().__init__(x, y, z) to initialize the x, y, and z coordinates of the vector.

The __repr__ the method is overridden to return a string representation of the PolarVector3d instance using its polar coordinates instead of its Cartesian coordinates.

The to_cartesian method is called a convenience method that allows you to create a Vector3d instance directly from polar coordinates without having to convert them manually first.

The Python statements below evaluate as follows:

Again, It appears that the PolarVector3d class has no attribute(method) dot. I will fix that in the next section Python: Overloading.

Overloading: Python

Python supports operator overloading, which allows you to define custom behavior for built-in operators like +, -, /, and *.

In this example, I've overloaded the negative sign(-), addition (+), subtraction (-), multiplication (*), and division (/) operators for the Vector3D class.

from typing import Tuple

class Vector3D:
    """A three-dimensional vector."""
    def __init__(self, x: float, y: float, z: float) -> None:
        """Initializes the vector with the given coordinates."""
        self.x: float = x
        self.y: float = y
        self.z: float = z
    def __str__(self) -> str:
        """Returns a human-readable string representation of the vector."""
        return "({}, {}, {})".format(self.x, self.y, self.z)
    def __repr__(self) -> str:
        """Returns an unambiguous string representation of the vector."""
        return "Vector3D({}, {}, {})".format(self.x, self.y, self.z)
    def __add__(self, other: 'Vector3D') -> 'Vector3D':
        """Returns the sum of the vector and the given vector."""
        return Vector3D(self.x + other.x, self.y + other.y, self.z + other.z)
    def __sub__(self, other: 'Vector3D') -> 'Vector3D':
        """Returns the difference of the vector and the given vector."""
        return Vector3D(self.x - other.x, self.y - other.y, self.z - other.z)
    def __mul__(self, scalar: float) -> 'Vector3D':
        """Returns the product of the vector and the given scalar."""
        return Vector3D(self.x * scalar, self.y * scalar, self.z * scalar)
    def __truediv__(self, scalar: float) -> 'Vector3D':
        """Returns the quotient of the vector and the given scalar."""
        return Vector3D(self.x / scalar, self.y / scalar, self.z / scalar)
    def __neg__(self) -> 'Vector3D':
        """Returns the negation of the vector."""
        return Vector3D(-self.x, -self.y, -self.z)
    def dot(self, other: 'Vector3D') -> float:
        """Returns the dot product of the vector and the given vector."""
        return self.x * other.x + self.y * other.y + self.z * other.z
    def cross(self, other: 'Vector3D') -> 'Vector3D':
        """Returns the cross product of the vector and the given vector."""
        return Vector3D(self.y * other.z - self.z * other.y,
                        self.z * other.x - self.x * other.z,
                        self.x * other.y - self.y * other.x)
    
    def magnitude(self) -> float:
        """Returns the magnitude of the vector."""
        return math.sqrt(self.x ** 2 + self.y ** 2 + self.z ** 2)
    def normalize(self) -> 'Vector3D':
        """Returns a normalized version of the vector."""
        return self / self.magnitude()
    def angle(self, other: 'Vector3D') -> float:
        """Returns the angle between the vector and the given vector in radians."""
        return math.acos(self.dot(other) / (self.magnitude() * other.magnitude()))        

Now that we have the method dot implemented, the Vector3d instances should evaluate successfully.

Yeah!

Double yeah!

Polymorphism: Python

Polymorphism in Python allows you to use a standard interface for different classes, which makes it easier to swap or combine different implementations.

In the following example, Vector3d and PolarVector3d classes can demonstrate polymorphism by implementing a standard method, such as info(), which returns a string representation of the vector in their respective coordinate systems.

def info(self):
    return f"Vector in Linear coordinates: {self.__str__()}"
    
setattr(Vector3d, "info", info)        
def info(self):
    return f"Vector in Polar coordinates: {self.__str__()}"
    
setattr(PolarVector3, "info", info)def display_vector_info(vector):
    print(vector.info())        
Note: IMHO, the above is a horrible abuse of Python’s setattr that hobbles maintenance. Wat a reviewer has asked me to do, was to go find the file the class is in and add the method. Sigh! They are right. I can think of only one good reason to use setattr.

Encapsulation: Python

Encapsulation in Python means hiding the internal details of a class and exposing only the necessary information and functionalities to the users. In our example, the Vector3d class can demonstrate encapsulation by using private attributes and exposing only the required methods to interact with the objects.

In the example below, the Vector3d class demonstrates encapsulation of the coordinates (x, y, z) by hiding the internal details of the class and exposing only the necessary information and functionalities.

def get_coordinates(self):
    return (self.x, self.y, self.z)

setattr(Vector3D, "get_coordinates", get_coordinates)
def set_coordinates(self, x, y, z):
    self.x = x
    self.y = y
    self.z = z
setattr(Vector3D, "set_coordinates", set_coordinates)        
Note: I used setattr again! This is not a code review. This is a blog and this is less confusing to the reader. IMHO.

OOP in Rust

I am switching modes from (my attempt at a) Pythonista to (my attempt at a) Rustic.

Note: I seriously doubt that Rustic is the correct name of the Rust clan. Set me straight in the comment section.

I used and showed Rust code in a Jupyter Notebook. I did not use cargo.You can find out how to set up the Jupyter Notebook for Rust here:

Optimizing My Rust Development Environment

We were commanded by the powers that be for our team to be the “guinea pig” that migrates to Rust. My approach entailed…

betterprogramming.pub

Rust Structs (similar to Python Classes) and Rust Traits

Rust’s struct type is similar to a class in object-oriented programming as it contains data and methods.

That is not quite the full truth struct <-> class, but before I get into that, an equivalent Rust struct of the Python Vector3d class is:

use std::fmt;
use std::f64::consts::PI;

#[derive(Copy, Clone)]
struct Vector3d {
    x: f64,
    y: f64,
    z: f64,
}
impl Vector3d {
    fn new(x: f64, y: f64, z: f64) -> Self {
        Vector3d { x, y, z }
    }
    fn magnitude(&self) -> f64 {
        (self.x.powi(2) + self.y.powi(2) + self.z.powi(2)).sqrt()
    }
    fn normalize(&self) -> Self {
        let magnitude = self.magnitude();
        *self / magnitude
    }
    fn angle(&self, other: &Vector3d) -> f64 {
        let dot_product = self.dot(other);
        let magnitudes_product = self.magnitude() * other.magnitude();
        (dot_product / magnitudes_product).acos()
    }
    fn dot(&self, other: &Vector3d) -> f64 {
        self.x * other.x + self.y * other.y + self.z * other.z
    }
}
impl fmt::Display for Vector3d {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {}, {})", self.x, self.y, self.z)
    }
}
impl fmt::Debug for Vector3d {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Vector3d({}, {}, {})", self.x, self.y, self.z)
    }
}
impl std::ops::Div<f64> for Vector3d {
    type Output = Vector3d;
    fn div(self, scalar: f64) -> Vector3d {
        Vector3d {
            x: self.x / scalar,
            y: self.y / scalar,
            z: self.z / scalar,
        }
    }
}
impl std::ops::Div<Vector3d> for Vector3d {
    type Output = Vector3d;
    fn div(self, other: Vector3d) -> Vector3d {
        Vector3d {
            x: self.x / other.x,
            y: self.y / other.y,
            z: self.z / other.z,
        }
    }
}        

Let us evaluate the above code and run it.

Anybody that knows Rust should know that what follows is illegal in a Rust environment.

Using proper terminology struct Vector3d is a new Type in Rust. Whereas Python Vector3d is a new class.

Rust is a strongly-typed functional language, and we already see a big difference.

The struct keyword is used to define new types in Rust. In this case, the struct keyword is used to define a new type called Vector3d. The Vector3dtype has three fields: x, y, and z. The x, y, and z fields are all of type f64.

The code defines Vector3d type has new methods (new, magintude, normilize,angle, and dot ) using the impl keyword.

Besides struct, I need something called a trait.

An aside on Rust traits

The following is a trait we used for struct Vector3d:

impl fmt::Display for Vector3d {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "({}, {}, {})", self.x, self.y, self.z)
    }
}        

Like Python, the __str__ method is the fmt::Display trait for the Vector3dstruct (type) in Rust. The fmt::Display trait provides a way to format and display a type in a human-readable form.

When I first started coding the above code, it took me a long time to figure out I needed a module, not an existing type.

Yup, fmt:: is not a type of Rust or any language I am aware of.

fmt:: is a module that provides types and traits for formatting and displaying values in Rust.

Now I must figure out the difference between a module and a crate.

This may be why they’re different.

The fmt:: module is part of the Rust standard library and provides functionality to format and display values in various ways.

use std::fmt;        

The above code should have been a clue. I missed it.

The following is another trait we used for struct Vector3d:

impl std::ops::Div<f64> for Vector3d {
    type Output = Vector3d;

fn div(self, scalar: f64) -> Vector3d {
        Vector3d {
            x: self.x / scalar,
            y: self.y / scalar,
            z: self.z / scalar,
        }
    }
}        

This Rust implementation enables division operation for references to Vector3d structs, where the divisor is an f64 value. Rust has a built-in trait used for division operations between two values using the / operator.

The implementation begins with the impl keyword, followed by the type parameters.

The Div<f64> syntax indicates that we are implementing the Div trait for the f64 type, which means that the divisor in the division operation will be an f64 value.

The type Output = Vector3d; line specifies that the output of the division operation will be a Vector3d struct.

The implementation then defines the div method, which refers to a Vector3d struct and an f64 value as arguments. A new Vector3d struct is created inside the method, with each field (x, y, and z) being divided by the f64 scalar value.

Can you figure out why we need this trait?

impl std::ops::Div<Vector3d> for Vector3d {
    type Output = Vector3d;

fn div(self, other: Vector3d) -> Vector3d {
        Vector3d {
            x: self.x / other.x,
            y: self.y / other.y,
            z: self.z / other.z,
        }
    }
}        

Another aside on Rust’s traits because they are essential

In Rust, a trait is like a blueprint that defines a bunch of methods that any type can implement — whether it’s a struct, an enum, or even a primitive type like an integer or a float.

By grossly bending the OOP concept, I create Python classes using Rust structs and traits. With traits, I will later accomplish overloading and polymorphism.

Traits are a powerful way to add functionality to your Rust code. They’re like interfaces in other languages, but they’re more flexible. You can use them to write generic code that works with any type that implements the trait. You can implement a trait for any type, even if it’s not in the same crate as the trait.

Using traits, we can define shared functionality across different types without writing separate functions for each.

Additionally, traits can be used to define operator overloading. We already had a sneak peek at overloading in the code above. We saw how the Div<f64> trait was implemented to allow the division of a Vector3d struct by a floating-point scalar value or a Vector3d Again, I will give more in detail in the section below, Rust: Overloading.

Inheritance: Rust

Inheritance is not a first-class concept in Rust, unlike in some other object-oriented languages like Python and Java.

The #[derive] attribute is used to derive traits’ implementations for a struct automatically. In this case, the #[derive(Debug)] attribute is used to implement the Debug traits for the struct automatically.

The Debug trait provides methods for printing the value of a struct in a human-readable format.

Using the #[derive] attribute can make your code more concise and easier to read.

Is this a form of inheritance?

Is there inheritance behavior in the following code?

use std::fmt;

struct Vector3d {
    x: f64,
    y: f64,
    z: f64,
}
impl fmt::Debug for Vector3d {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "Vector3d {{ x: {}, y: {}, z: {} }}",
            self.x, self.y, self.z
        )
    }
}        

By inheriting using derive[], the resulting code becomes much more succinct:

#[derive(Debug)]
struct Vector3d {
    x: f64,
    y: f64,
    z: f64,
}        

The #[derive] attribute is a powerful tool that can help you write more concise and easier-to-read Rust code.

Inheritance and Traits in Rust

Traits are promised to be a powerful feature of Rust that can be used to improve the modularity and reusability of our code.

The same is true for Python inheritance.

For Rust, I can use traits and composition for OOP inheritance. I have to figure it out first.

Note: There are other ways, four of which are called “ deriving traits”, “composition”, “wrapping” , or “boxing” a type, which I will look at later, but not in this article.

Overloading: Rust

Rust supports operator overloading, which allows you to define custom behavior for built-in operators like +, -, /, and *. Rust requires you to define operator methods for any new vector, matrix, tensor, or n-D type.

use std::ops::{Add, Sub, Mul, Div};

#[derive(Debug, Clone, Copy)]
struct Vector3d {
    x: f64,
    y: f64,
    z: f64,
}
impl Add for Vector3d {
    type Output = Vector3d;
    fn add(self, other: Vector3d) -> Vector3d {
        Vector3d {
            x: self.x + other.x,
            y: self.y + other.y,
            z: self.z + other.z,
        }
    }
}
impl Sub for Vector3d {
    type Output = Vector3d;
    fn sub(self, other: Vector3d) -> Vector3d {
        Vector3d {
            x: self.x - other.x,
            y: self.y - other.y,
            z: self.z - other.z,
        }
    }
}
impl Mul<f64> for Vector3d {
    type Output = Vector3d;
    fn mul(self, scalar: f64) -> Vector3d {
        Vector3d {
            x: self.x * scalar,
            y: self.y * scalar,
            z: self.z * scalar,
        }
    }
}
impl Mul<Vector3d> for Vector3d {
    type Output = Vector3d;
    fn mul(self, other: Vector3d) -> Vector3d {
        Vector3d {
            x: self.x * other.x,
            y: self.y * other.y,
            z: self.z * other.z,
        }
    }
}
impl Div<f64> for Vector3d {
    type Output = Vector3d;
    fn div(self, scalar: f64) -> Vector3d {
        Vector3d {
            x: self.x / scalar,
            y: self.y / scalar,
            z: self.z / scalar,
        }
    }
}
impl Div<Vector3d> for Vector3d {
    type Output = Vector3d;
    fn div(self, other: Vector3d) -> Vector3d {
        Vector3d {
            x: self.x / other.x,
            y: self.y / other.y,
            z: self.z / other.z,
        }
    }
}
impl Vector3d {
    fn cross(&self, other: &Vector3d) -> Vector3d {
        Vector3d {
            x: self.y * other.z - self.z * other.y,
            y: self.z * other.x - self.x * other.z,
            z: self.x * other.y - self.y * other.x, 
        }
    }
}        

One of the mul() methods is wrong. Please find it and fix it.

Polymorphism: Rust

Polymorphism in Rust refers to the ability of a type to take on multiple forms. In Rust, polymorphism is achieved through the use of traits. A trait is a collection of methods that types can implement, allowing them to share common behavior.

Polymorphism in Rust can be static or dynamic.

Dynamic Polymorphism: Rust

Dynamic anything in Rust surprises me.

It turns out I was using dynamic polymorphism in the Overloading: Rust section above.

The Add and Sub traits were used to define the add() and sub() methods for the Vector3d struct. These methods take two Vector3d instances as arguments and return a new Vector3d instance.

The Add and Sub traits are generic traits. This means that they can be used with any type that implements them.

In the case of the Vector3d struct, the Add and Sub traits are implemented by the Vector3d struct itself. The add() and sub() methods are used with any two Vector3d instances, regardless of their specific values.

Here is a small part of the Vector3d type implementation:

use std::ops::{Add, Sub, Mul, Div};

#[derive(Debug, Clone, Copy)]
struct Vector3d {
    x: f64,
    y: f64,
    z: f64,
}
impl Add for Vector3d {
    type Output = Vector3d;
    fn add(self, other: Vector3d) -> Vector3d {
        Vector3d {
            x: self.x + other.x,
            y: self.y + other.y,
            z: self.z + other.z,
        }
    }
}

impl Sub for Vector3d {
    type Output = Vector3d;
    fn sub(self, other: Vector3d) -> Vector3d {
        Vector3d {
            x: self.x - other.x,
            y: self.y - other.y,
            z: self.z - other.z,
        }
    }
}        

Dynamic polymorphism is accomplished through the use of trait objects. Trait objects are created by specifying a trait as a type, allowing a variable to hold any value that implements that trait. This allows for more flexibility at runtime, as the exact type of the object can be determined dynamically.

Static polymorphism: Rust

Static polymorphism, also known as compile-time polymorphism, is achieved through generics. Generic types allow code to be written in a way that can handle multiple types, making the code more reusable.

The following code adds the second type Sphericalector3d and the operators Add, Sub, Mul, and Div.

use std::ops::{Add, Sub, Mul, Div};
use std::fmt;

#[derive(Clone, Copy)]
struct SphericalVector3d {
    r: f64,
    theta: f64,
    phi: f64,
}
impl Add for SphericalVector3d {
    type Output = SphericalVector3d;
    fn add(self, other: SphericalVector3d) -> SphericalVector3d {
        SphericalVector3d {
            r: self.r + other.r,
            theta: self.theta + other.theta,
            phi: self.phi + other.phi,
        }
    }
}
impl Sub for SphericalVector3d {
    type Output = SphericalVector3d;
    fn sub(self, other: SphericalVector3d) -> SphericalVector3d {
        SphericalVector3d {
            r: self.r - other.r,
            theta: self.theta - other.theta,
            phi: self.phi - other.phi,
        }
    }
}
impl Mul<f64> for SphericalVector3d {
    type Output = SphericalVector3d;
    fn mul(self, scalar: f64) -> SphericalVector3d {
        SphericalVector3d {
            r: self.r * scalar,
            theta: self.theta * scalar,
            phi: self.phi * scalar,
        }
    }
}
impl Div<f64> for SphericalVector3d {
    type Output = SphericalVector3d;
    fn div(self, scalar: f64) -> SphericalVector3d {
        SphericalVector3d {
            r: self.r / scalar,
            theta: self.theta / scalar,
            phi: self.phi / scalar,
        }
    }
}
impl fmt::Display for SphericalVector3d {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "SphericalVector3d({}, {}, {})", self.r, self.theta, self.phi)
    }
}
impl fmt::Debug for SphericalVector3d {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "SphericalVector3d({}, {}, {})", self.r, self.theta, self.phi)
    }
}
fn staticPoly() {
    let s1 = SphericalVector3d { r: 1.0, theta: 2.0, phi: 3.0 };
    let s2 = SphericalVector3d { r: 4.0,  theta: 12.0, phi: 13.0 };
    
    let s3 = s1 + s2;
    println!("s3 = {}", s3);
}
staticPoly()        

The above output shows that the second type Sphericalector3d and the operators Add, Sub, Mul, and Div accomplish static polymorphism.

Encapsulation: Rust

Encapsulation is a crucial principle of object-oriented programming. It involves hiding the internal implementation details of a class or struct and exposing only a public interface for interacting with its data. This makes the code more modular and easier to maintain.

In the following code, the Vector3d struct encapsulates its implementation details by making its data members (x, y, and z) private. This means that these members are not accessible from outside the struct. The only way to access them is through the public methods of the struct.

use std::fmt;

struct Vector {
    x: f64,
    y: f64,
    z: f64,
}
impl Vector {
    pub fn new(x: f64, y: f64, z: f64) -> Self {
        Vector { x, y, z }
    }
    pub fn get_coordinates(&self) -> (f64, f64, f64) {
        (self.x, self.y, self.z)
    }
    pub fn set_coordinates(&mut self, x: f64, y: f64, z: f64) {
        self.x = x;
        self.y = y;
        self.z = z;
    }
}
impl fmt::Display for Vector {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {}, {})", self.x, self.y, self.z)
    }
}
fn ex_encapsulation() {
    let mut v1 = Vector::new(1.0, 2.0, 3.0);
    println!("{}", v1);
    println!("Cartesian coordinates of v1: {:?}", v1.get_coordinates());
    v1.set_coordinates(4.0, 5.0, 6.0);
    println!("Updated Cartesian coordinates of v1: {:?}", v1.get_coordinates());
}
ex_encapsulation()        

The new function is used to create new instances of the Vector3d struct and initialize its private data members. The get_coordinates function provides read-only access to the private data members of the struct by returning a tuple containing the x, y, and z values. The set_coordinates function provides a way to modify the private data members of the struct from the outside world, but only through the public interface provided by the Vector3d struct.

Overall, by encapsulating the implementation details of the Vector3d struct, the code reduces the complexity and improves the maintainability of the overall system. This is because other parts of the system can interact with the Vector3d struct using its public interface without worrying about the underlying implementation details.

OOP in Rust: Review

Rust is a strongly-typed language. Strongly-typed languages are like the code reviewer I used to know. Code reviewers, good ones, check up on you and ensure you’re doing everything correctly. They might be annoying sometimes, but they always seek your best interests.

Strong-typed languages, i.e., Rust, can be more challenging to learn than weakly-typed languages, i.e., Python, but Rust is much faster. However, the syntax, using:;{}[]*/\ and some others, is like programming in regex, only with more rules. I need three hands!

Rust is a multi-paradigm programming language that supports procedural, functional programming, and, with some imagination, actual object-oriented programming (OOP) styles. While Rust does not have all the traditional OOP features that some other languages have, it offers several common behaviors in OOP.

Several key object-oriented behaviors in Rust allow you to define and work with custom types similar to other object-oriented languages.

  1. You can define structs in Rust, similar to classes in other object-oriented languages. Structs can have data members and methods, just like classes.
  2. Rust has a feature called traits, which defines a set of methods that can be implemented by any type that wants to provide that behavior. This is similar to interfaces in other object-oriented languages, where you define a contract that a type must follow to provide specific behavior.
  3. Rust allows you to define implementations, which define how a particular struct or type implements a trait. This is similar to defining methods for a class in other object-oriented languages.
  4. Rust supports generics, which allow you to define a type or function that can work with different types. This is similar to template classes and functions in C++, where you can define a general implementation that can be used with different types.
  5. Rust supports dynamic method dispatch, which allows you to call a method on a struct or type through a trait object. This is similar to virtual function calls in C++, where you can call a method on an object without knowing its exact type at compile time.

These five key object-oriented behaviors in Rust provide a powerful and flexible way to define custom types and behavior, similar to other object-oriented languages.

I like Rust because:

  • It does not have a Python GIL. Rust can easily use modern chipsets with multiple cores.
  • It is more memory safe than C and C++.
  • It is 30+ times faster than Python.

Remember you get the OOP and all associated code by cloning the GitHub repo at https://github.com/bcottman/OOP/ .

Resources

The Rust Programming Language

The first step is to install Rust. We’ll download Rust through rustup, a command line tool for managing Rust versions…

doc.rust-lang.org

Rust Design Patterns

If you are interested in contributing to this book, check out the contribution guidelines. In software development, we…

rust-unofficial.github.io

The Rust Programming Language

Object-oriented programming (OOP) is a way of modeling programs. Objects as a programmatic concept were introduced in…

doc.rust-lang.org

Is Rust an Object-Oriented Programming Language?

Python-based compiler achieves orders-of-magnitude speedups

In 2018, the Economist published an in-depth piece on the programming language Python. “In the past 12 months,” the…

news.mit.edu

Optimizing My Rust Development Environment

We were commanded by the powers that be for our team to be the “guinea pig” that migrates to Rust. My approach entailed…

betterprogramming.pub

Code safely!


要查看或添加评论,请登录

Bruce Cottman的更多文章

社区洞察

其他会员也浏览了