Back to Home

Generic type closure in Rust

rust · generics · closure · downcast · Any · generic types · closure · cast

Generic type closure in Rust


    In this short article, I will talk about a pattern in Rust that allows you to "save" for later use a type passed through a generic method. This pattern is found in the source code of Rust libraries and I also sometimes use it in my projects. I could not find publications about him in the network, so I gave him my name: "Generalized type closure", and in this article I want to tell you what it is, why and how it can be used.


    Problem


    In Rust, a developed static type system and its static capabilities are enough for probably 80% of cases. But it happens that dynamic typing is needed when you want to store objects of different types in the same place. Character types-objects come to the rescue here: they erase the real types of objects, reduce them to a certain general interface defined by the type, and then you can operate on these objects as the same type-types-objects.


    This works well in half of the remaining cases. But what if we still need to restore the erased types of objects when using them? For example, if the behavior of our objects is determined by a type that cannot be used as a type-object . This is a common situation for traits with associated types. What to do in this case?


    Decision


    For all 'statictypes (that is, types that do not contain non-static references), Rust implements a type Anythat allows the conversion of the type-object dyn Anyto a link to the original type of the object:


    let value = "test".to_string();
    let value_any = &value as &dyn Any;
    // Пытаемся привести наше значение к типу String. Если
    // не получилось - значит наше значение имеет другой тип.
    if let Some(as_string) = value_any.downcast_ref::() {
        println!("String: {}", as_string);
    } else {
        println!("Unknown type");
    }

    Run


    Also, Boxfor these purposes there is a method downcast.


    This solution is suitable for those cases when the source type is known at the place of work with it. But what if it is not so? What if the calling code simply does not know about the source type of the object in the place of its use? Then we need to somehow remember the original type, take it where it is defined, and save it along with the object type dyn Any, so that later the latter is converted to the original type in the right place.


    Generalized types in Rust can be treated as type variables into which one or another type value can be passed when called. But in Rust there is no way to remember this type for further use elsewhere. However, there is a way to remember all the functionality using this type, along with this type. This is the idea of ​​the "Closing of a generalized type" pattern: code using a type is executed in the form of a closure, which is stored as a normal function, because it does not use any objects of the environment, except for generalized types.


    Implementation


    Let's look at an implementation example. Suppose we want to make a recursive tree that represents a hierarchy of graphic objects, in which each node can be either a graphic primitive with child nodes, or a component - a separate tree of graphic objects:


    enum Node {
        Prim(Primitive),
        Comp(Component),
    }
    struct Primitive {
        shape: Shape,
        children: Vec,
    }
    struct Component {
        node: Box,
    }
    enum Shape {
        Rectangle,
        Circle,
    }

    Packaging Nodein the structure is Componentnecessary, since the structure itself is Componentused in Node.


    Now suppose that our tree is just a representation of some model with which it should be associated. Moreover, each component will have its own model:


    struct Primitive {
        shape: Shape,
        children: Vec>,
    }
    struct Component {
        node: Box>,
        model: Model,   // Компонент содержит Model
    }

    We could write:


    enum Node {
        Prim(Primitive),
        Comp(Component),
    }

    But this code will not work as we need. Because the component must have its own model, and not the model of the parent element, which contains the component. That is, we need:


    enum Node {
        Prim(Primitive),
        Comp(Component),
    }
    struct Primitive {
        shape: Shape,
        children: Vec>,
        _model: PhantomData, // Имитируем использование Model
    }
    struct Component {
        node: Box,
        model: Box,
    }
    impl Component {
        fn new(node: Node, model: Model) -> Self {
            Self {
                node: Box::new(node),
                model: Box::new(model),
            }
        }
    }

    Run


    We have moved the indication of a specific type of model to the method new, and in the component itself we store the model and subtree already with deleted types.


    Now add a method use_modelthat will use the model, but will not be parameterized by its type:


    struct Component {
        node: Box,
        model: Box,
        use_model_closure: fn(&Component),
    }
    impl Component {
        fn new(node: Node, model: Model) -> Self {
            let use_model_closure = |comp: &Component| {
                comp.model.downcast_ref::().unwrap();
            };
            Self {
                node: Box::new(node),
                model: Box::new(model),
                use_model_closure,
            }
        }
        fn use_model(&self) {
            (self.use_model_closure)(self);
        }
    }

    Run


    Note that in the component we store a pointer to a function that is created in the method newusing the syntax for defining a closure. But all that it should capture from the outside is a type Model, so we are forced to pass a link to the component itself into this function through an argument.


    It seems that instead of closure, we can use an internal function, but such code does not compile. Because the internal function in Rust cannot capture generalized types from the external one due to the fact that it differs from the usual top-level function only in visibility.

    Now the method use_modelcan be used in a context where the real type is Modelunknown. For example, in a recursive tree traversal consisting of many different components with different models.


    Alternative


    If it is possible to transfer the component’s interface to a type that allows the creation of a type-object, then it’s better to do so, and instead use the component itself to operate on its type-object:


    enum Node {
        Prim(Primitive),
        Comp(Box),
    }
    struct Component {
        node: Node,
        model: Model,
    }
    impl Component {
        fn new(node: Node, model: Model) -> Self {
            Self {
                node,
                model,
            }
        }
    }
    trait ComponentApi {
        fn use_model(&self);
    }
    impl ComponentApi for Component {
        fn use_model(&self) {
            &self.model;
        }
    }

    Run


    Conclusion


    It turns out that closures in Rust can capture not only environment objects, but also types. However, they can be interpreted as ordinary functions. This property becomes useful when you need to work uniformly with different types without losing information about them, if character types are not applicable.


    I hope this article helps you use Rust. Share your thoughts in the comments.

    Read Next