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


Functions and closures

A function consists of a type definition (function signature) and an execution environment (imperative code block). Before turning to the implementation, let’s start with a simple function:

struct Line<P> {
    points: (P, P),
}

fn connect<P>(left: P, right: P) -> Line<P> {
    Line {
        points: (left, right)
    }
}
Copy the code

Here, the type used in the Point tuple is determined where connect is called, because that’s where Line is called. If tight coupling is acceptable, we can specify that connect always handles Lines and Points, while still allowing a generic type for the ID attribute of a Point.

fn connect<Id>(left: Point<Id>, right: Point<Id>) -> Line<Point<Id>> {
    Line {
        points: (left, right)
    }
}
Copy the code

We still have a type parameter, but now the Point and Line types are tightly coupled in this function.

As with any function, arguments can be written as a structure, and the function body can be moved into a method. We don’t want to win the eternal battle between functional and object-oriented code aesthetics, but simply observe that function signatures are type definitions. This is not a trivial observation. For now, it is enough to point this out. Let’s stick with the generic type of closure.

/// This is the same Point we used to first demonstrate a generic type.
#[derive(Debug)]
struct Point<Id> {
    id: Id
}

/// This Line is different. We've restricted it to use the Point type but left
/// the Id flexible. Before, this tight coupling was done in the `connect`
// method.
#[derive(Debug)]
struct Line<Id> {
    points: (Point<Id>, Point<Id>),
}

/// This new type contains a closure. While Box is a type, this involves Fn, a
/// trait that uses generic types. We will write our own such trait later.
struct Connector<Id> {
    pub op: Box<dyn Fn(Point<Id>, Point<Id>) -> Line<Id>>,
}

fn main() {
    let connector: Connector<String> = Connector {
        op: Box::new(|left, right| Line {
            points: (left, right),
        }),
    };

    let point_a = Point {
        id: "A".to_string(),
    };
    let point_b = Point {
        id: "B".to_string(),
    };
    let line = (connector.op)(point_a, point_b);

    println!("{:? }", line);
}
Copy the code

Here, we’ve built a little library that, with a little more work, can do some interesting things.

If we iterate over pairs of points, we can allow users to concatenate them while recording, modifying, or filtering them. At the same time, our library can handle the iterative way of working. Alternatively, instead of iterating over pairs and returning lines, we can select them from any size group the user wants and combine them into the type we provide. We can also let users write out their own lines, squares, graphics, or whatever they want, and let them insert it into a type parameter (that is, a generic type).