“This is the fifth day of my participation in the Gwen Challenge in November. Check out the details: The Last Gwen Challenge in 2021.”


Part2 introduces

The first part of the code leaves us with a cool library, but it only scratches the surface of the power of generics. This section will introduce: Implementations and Traits.

At the end of part 1, we used the Fn feature, but we used dyn inside the Box. The dyn keyword means that we are using a run-time abstraction, and Box stands for heap allocation. We’ll cancel Box

> when we dig into the most powerful part of the type system.

Why do we need more complexity?

Looking at the first part of our code, we’ve decided:

  • Point has its own Id
  • A Line must have exactly two points
  • A connector can do anything a library user wants at run time

As long as it takes two points and produces a line.

Our main function prints our Line Instance to the console using the debug format. However, if we want to ensure that each connector creates a line that can be displayed in debug format (i.e. “{:? } “)?

How can we be selective enough to make sure this always works? What if we want it to work without forcing the Debug type? This is what we call selective flexibility.

We can write: Line

. The trait binding restricts the Id type to those that implement the Debug attribute. But there are compelling reasons to keep our type definitions as simple as possible. Most importantly, unless all fields are public, we will not be able to construct this type from outside its module.

Therefore, we simply can’t do anything with it without using at least one related function, so a new approach is needed. This is why trait constraints are almost always written in impL blocks rather than type definitions.

There are many types in STD ::iter that are designed to hold a closure, but to see what kind of closure it is, we have to look at the associated implants (e.g., Map). When you learn how to do something by reading library code, you know you’re doing it right. But be sure to work gradually and always understand what you’re writing. Let’s take a look at the process now.

implementation

The definition of a type (struct/enum) describes what it is. Its implementation (IMPL) describes what it can do. When we parameterize a type and restrict it in an implant, we describe what the type can do when the implant condition is satisfied.

Let’s start with what we discussed above: allow Line to be debugable while Id is debugable.

use std::fmt::{self.Debug, Formatter};

// Remember to remove `#[derive(Debug)]` from `Line`.
impl<Id: Debug> Debug for Line<Id> {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.debug_struct("Line").field("points", &self.points).finish()
    }
}
Copy the code

It is important to know that this type is still flexible enough to accommodate non-Debug types.

This implementation simply says: “Line can use Debug only if Id can use Debug”. The structure Line

indicates that Line cannot exist unless the Id implements Debug. We just do what’s happening behind the scenes with #[derive(Debug)] (i.e. macro annotations directly on Line). It’s not magic, it’s creative coding!

Now, we should be clear about what our goal is: The great thing about Rust’s combination of flexibility and clarity is that when the tool is understood and the goal is clear, the code is actually written by itself.

We will turn Connector into an iterator so that it can create Line instances from a collection of Point instances. Later, we’ll make it possible to create any type of shape, not just lines, using any number of points, not just two. But we will do this gradually so that we fully understand our code.

At the end of the second part, the user of our library will put some points into any collection (not just a Vec) and call a method. They’ll let it know how to combine those points with a closure. This is similar to how the library uses the Collect method to turn iterators into collections, or the map method to change the type or value in an iterator.

We’ll start by making our iterators look more like those in the standard library. This means replacing Box

with an unbounded generic type and specifying a type in the IMPl Block that implements Fn’s features. As usual, the standard library provides the best example of what is right:

struct Connector<Id, F> {
    points: Vec<Point<Id>>,
    op: F,
}

impl<Id, F> Connector<Id, F>
where
    F: Fn(Point<Id>, Point<Id>) -> Line<Id>,
{
    fn new(points: Vec<Point<Id>>, op: F) -> Self {
        Connector { points, op }
    }
}
Copy the code