How to quickly customize a collection type? A programmer familiar with some object-oriented languages might write it like this.

1
2
class MyCollection(Collection):
    ...

Inherit some built-in type (if it exists) and extend on top of that built-in type. But for programmers familiar with Python, this is not a good idea.

Types of ducks

When a bird is seen walking like a duck, swimming like a duck, and quacking like a duck, then the bird can be called a duck.

A simple example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Collection:
    def __init__(self, nums):
        self._nums = list(nums)
    
    def __len__(self):
        return len(self._nums)
    
    def __getitem__(self, position):
        return self._nums[position]
    
c = Collection(['A', 'B', 'C', 'D', 'E', 'F'])

# 可以对c调用内置的len方法获取长度
print(len(c))可以

# 可以使用索引
print(c[2])

# 可以使用for循环遍历元素
for i in c:
    print(i)
    
# 还可以使用切片操作 
print(c[1:3])

See, this custom Collection class doesn’t inherit from Python’s built-in Iterable type, but behaves as if it were some built-in iterable type.

These double underscore methods are generally called magic methods, and are not supposed to be called by the user; they are treated by the interpreter as a “protocol” that allows your custom class to have features like indexing, slicing, etc., regardless of whether it inherits from a standard library type, as long as it implements the corresponding protocol .

Python doesn’t care about whether our custom Collection type is a subclass of a collection type, but whether it has the “ability” to be a collection.

The following example shows that Python doesn’t even require that the interface be implemented when defining the class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from collections.abc import Iterable

class Statement:
    def __init__(self, string):
        self.words = string.split()
        
def my_iter(self):
    for word in self.words:
        yield word

Statement.__iter__ = my_iter
        
s = Statement("Python is a programming language that lets you work quickly and integrate systems more effectively")

print(list(iter(s)))

print(isinstance(s, Iterable)) # True

By dynamically registering the __iter__ method of the Statement class directly at runtime (this is called a monkey patch by the community and I personally don’t like it in real projects), you can call the iter function on this custom type and even isinstance(s, Iterable) returns a True result.

Mixin

I gave an example in a sharing session if there is such an inheritance chain in the system.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 伪代码

class 飞机:
    def 起飞:
        pass

    def 剩余油量:
        pass

class 直升机(飞机):
    pass

class 战斗机(飞机):
    pass

If a new flying object needs to be introduced in this system, but it represents a seagull, how to do it? Inherit the existing aircraft base class directly? If we do that we will get a steel seagull with fuel quantity information, a unique new species. Or we could extract another common abstract base class that has only the parts common to aircraft and seagulls.

But if we think in terms of duck types and add a new type, why must we be sure that it is a subclass of some type? Whether it’s a helicopter or a seagull, all they need in common is the ability to fly.

Django is one of the popular web frameworks in Python, and views can be defined in Django like this.

1
2
3
4
5
6
7
class HybridDetailView(JSONResponseMixin, SingleObjectTemplateResponseMixin, BaseDetailView):
    def render_to_response(self, context):
        # Look for a 'format=json' GET argument
        if self.request.GET.get('format') == 'json':
            return self.render_to_json_response(context)
        else:
            return super().render_to_response(context)

Instead of defining a comprehensive parent class, Django defines multiple Mixin classes, which are usually translated into Chinese as mixin classes, each of which is responsible for a part of the functionality, for example JSONResponseMixin is responsible for JSON responses, which is somewhat similar to CSharp, Java’s interface (interface). The difference is that CSharp doesn’t support multiple inheritance, but allows multiple interfaces, while Mixin in Python is just a default convention that everyone follows, and can be written this way because Python supports multiple inheritance, and a child class can inherit from multiple parents. The Mixin should not affect the functionality of the subclass itself; it should abstract a generic function for extending the subclass, which itself cannot normally be instantiated.

Such code formally inherits from multiple parent classes, but in practical terms it is more like a combination of functionality from different mixin classes. For example, the above code combines the functionality of a JSON response with a template response, returning different types of responses depending on the request. Mixing duck calls, shapes, and flight patterns gives you a custom “duck”, depending on what functionality you need.

Object-Orientation in ## Rust

Now it’s the turn of Rust, a programming language that supports multiple paradigms, including the object-oriented paradigm. But first, what exactly is object-oriented? To borrow from the official tutorial The Book: If you follow the GOF description of object-oriented Patterns), object-oriented programs consist of objects that package together data and the processes that manipulate it, then Rust undoubtedly supports object orientation, with Rust organizing data by enum and struct and binding methods to them via impl.

However, some programmers may object to this statement, as some people believe that only forms with encapsulation, inheritance, and polymorphism count as object-oriented, and Rust doesn’t even have class, just as some people believe that JS and Python don’t quite count as object-oriented languages either.

Encapsulation, Inheritance, Polymorphism

These three words are really deep, and chances are that every software engineer has heard of them, so here’s a discussion of these three features in Rust.

The main role of encapsulation, in my opinion, is to isolate different levels of abstraction, where the bottom developer is responsible for the implementation details, and the developer at the top is only concerned with the exposed interface. For example, for list in Python, we know that it has an interface that lets us get the number of elements inside it without having to go into the internal implementation details, which are the responsibility of the standard library developers. If we encapsulate a minimum stack on top of this object, we can get the minimum value in the list via the min method, and we are responsible for encapsulating this interface, whether we maintain a separate stack to hold the minimum value or traverse the entire list when we call the interface is an internal detail that users of this type do not need to know.

Of course, for some languages, mechanisms are provided to force properties to be hidden from external callers, and Rust has the pub keyword to restrict accessibility .

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
mod my_test {
    pub struct Test {
        foo: i32,
        pub bar: i32,
    }
}

fn main() {
    use my_test::Test;

    let test = Test { foo: 1, bar: 2}; // 错误
}

Since the foo field is not identified by the pub keyword, it is a private field and cannot be accessed directly.

Next is inheritance, and there is no inheritance in Rust . It is not possible to implement a child structure that inherits from a parent structure. Inheritance has two main purposes, one is to reuse code, where the child class automatically gets the properties and methods of the parent class, but code reuse does not necessarily require inheritance; the other is for polymorphism, where a child type can be used where the parent type is needed.

This makes it seem a bit odd to compare the concepts of polymorphism and inheritance. Inheritance becomes a way to implement polymorphism, which is a bit broader.

Since Rust doesn’t have inheritance, how are the two functions inherited above (mainly the latter) going to be implemented in Rust? How would polymorphism be implemented?

Rust can abstract shared behavior through trait. Take the example of an airplane, where all kinds of airplanes, and seagulls, can fly, but the specific way they fly is a little different.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
trait Fly {
    fn fly(&self);
}

struct Helicopter;

// 为直升机实现飞行特性
impl Fly for Helicopter {
    fn fly(&self) {
        println!("转动螺旋桨起飞");
    }
}

struct Seagull;

impl Fly for Seagull {
    fn fly(&self) {
        println!("扇动翅膀起飞");
    }
}

The syntax impl trait for struct/enum allows a function to be abstracted and implemented for different types, and in contrast to Python’s Mixin, trait can also be combined to implement multiple traits for a single type. Like Mixin and C# interfaces, traits can have default implementations.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
trait Fly {
    fn fly(&self);
}

trait Dashboard {
    // 可以提供默认实现,当然在这里只是演示用,无意义
    fn speed(&self) -> i32 {
        100
    }
}

struct Helicopter;

impl Fly for Helicopter {
    fn fly(&self) {
        println!("转动螺旋桨起飞");
    }
}

// 可以使用默认实现,也可以覆盖默认行为
impl Dashboard for Helicopter {}

The core idea of trait is combination, trait is an abstraction of behavior, different objects can have similar behavior, objects are a combination of data and behavior. In the previous Django example, although syntactically it is multiple inheritance, isn’t it also essentially a combination? Compositions are better suited than inheritance to indicate that an object has a certain function or feature, rather than is a certain kind.

Look at the duck type. When a place needs a duck that quacks, as long as we provide an object with the quack of a duck, isn’t that just polymorphism? So how is polymorphism represented in Rust’s type system?

The following code will compile through.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
enum Status {
    Successful,
    Failed,
}

fn print_status(status: Status) {
    match status {
        Status::Successful => println!("successful!"),
        Status::Failed => println!("failed")
    }
}

fn main() {
    let status = Status::Successful;
    print_status(status);
}

Of course, Rust’s enumeration is a and type on the type system, where Status::Successful and Status::Failed are of the same type (Status), often referred to as variants, and look at another code example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 省略了前面的结构体与trait定义部分

fn main() {
    let h = Helicopter;
    let s = Seagull;
    generic_func(h);
    generic_func(s);
}

fn generic_func<T: Fly>(flyable: T) {
    flyable.fly();
}

The code can be compiled, where I make use of generics, where the custom function takes a parameter of type T, which is qualified as: the type that implements the trait Fly, which is called trait bounds.

The code has been slightly modified.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
use std::fmt::Debug;

trait Fly {
    fn fly(&self);
}

#[derive(Debug)]
struct Helicopter;

impl Fly for Helicopter {
    fn fly(&self) {
        println!("转动螺旋桨起飞");
    }
}

#[derive(Debug)]
struct Seagull;

impl Fly for Seagull {
    fn fly(&self) {
        println!("扇动翅膀起飞");
    }
}

fn main() {
    let h = Helicopter;
    let s = Seagull;
    generic_func(h);
    generic_func(s);
}

fn generic_func<T: Fly + Debug>(flyable: T) {
    println!("正在飞行的是:{:?}", flyable);
    flyable.fly();
}

The Debug trait is implemented here for both structs via the derive macro. By implementing this trait, you can print out the name of the structure itself. To add this trait to the type qualification of the generic method, the syntax T: trait1 + trait2 can be used to qualify that a type must implement multiple traits.

The printed result is.

1
2
3
4
正在飞行的是:Helicopter
转动螺旋桨起飞
正在飞行的是:Seagull
扇动翅膀起飞

Writing code with only one generic function and Rust actually creating separate functions for each different type after compilation is an approach called static distribution which has the disadvantage of making the compiled size larger. Another approach, called dynamic distribution, puts the type determination at runtime, which takes up less space but introduces more runtime overhead.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn main() {
    let h = Helicopter;
    let s = Seagull;
    generic_func(&h);
    generic_func(&s);
}

fn generic_func(flyable: &dyn Fly) {
    flyable.fly();
}

The code is not changed much, and dynamic distribution can be achieved by & borrowing or Box smart pointer wrapper type, and to add dyn keyword.

Off topic: Generic polymorphism is not just for trait bounds, see reference and other sources.

This is a statically typed “duck type” belonging to Rust, generic_func needs objects that can fly, not caring whether they are airplanes or seagulls, and not caring whether they have a common parent class.

Summary

The main purpose of this article is to show how object-oriented programming can be implemented in a Rust way. Rust is not completely unique, and the Python example is listed to illustrate this point. In addition, object-oriented is not the same as encapsulation, inheritance, polymorphism, inheritance and polymorphism can not even be considered parallel concepts.

As for the detailed usage of generic and trait in Rust, it is limited to space, and related materials such as the official documentation are very detailed, so I won’t elaborate.