📜 ⬆️ ⬇️

Generalized closure in Rust


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


Problem


In Rust, the developed static type system and its static capabilities are enough for probably 80% of use cases. But it happens that dynamic typing is needed when it is necessary to store objects of different types in the same place. Here types-objects come to the rescue: they erase the real types of objects, reduce them to some kind of common interface defined by the type, and then you can operate with these objects as single-type objects.


This works well in another 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 set by such a type that cannot be used as a type-object . This is a common situation for types with associated types. How to be in this case?


Decision


For all 'static types (that is, types that do not contain non-static links), Rust implements the type Any , which allows the dyn Any type-object to be converted to a reference to the original object type:


 let value = "test".to_string(); let value_any = &value as &dyn Any; //       String.  //   -      . if let Some(as_string) = value_any.downcast_ref::<String>() { println!("String: {}", as_string); } else { println!("Unknown type"); } 

Launch


Box for this purpose also has a downcast method.


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


Generalized types in Rust can be treated as type variables to which certain type values ​​can be passed when called. But in Rust there is no way to remember this type for its further use elsewhere. However, there is a way to remember all the functionality that uses this type, along with this type. This is the idea of ​​the "Closure of a generic type" pattern: code using a type is drawn up as a closure, which is stored as a normal function, because it does not use any environment objects, except for generic types.


Implementation


Let's look at an example implementation. Suppose we want to make a recursive tree representing 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<Node>, } struct Component { node: Box<Node>, } enum Shape { Rectangle, Circle, } 

The packaging of a Node in the Component structure is necessary, since the Component itself is used in the Node .


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


 struct Primitive<Model> { shape: Shape, children: Vec<Node<Model>>, } struct Component<Model> { node: Box<Node<Model>>, model: Model, //   Model } 

We could write:


 enum Node<Model> { Prim(Primitive<Model>), Comp(Component<Model>), } 

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 that contains the component. That is, we need:


 enum Node<Model> { Prim(Primitive<Model>), Comp(Component), } struct Primitive<Model> { shape: Shape, children: Vec<Node<Model>>, _model: PhantomData<Model>, //   Model } struct Component { node: Box<dyn Any>, model: Box<dyn Any>, } impl Component { fn new<Model: 'static>(node: Node<Model>, model: Model) -> Self { Self { node: Box::new(node), model: Box::new(model), } } } 

Launch


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


Now we add the use_model method, which will use the model, but will not be parameterized by its type:


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

Launch


Notice that in the component we store a pointer to the function that was created in the new method using the closure definition syntax. But all that it has to capture from the outside is the Model type, so we are forced to pass the reference to the component itself to this function through an argument.


It seems that instead of a closure, we can use an internal function, but such code will 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 by visibility.

Now the use_model method can be used in a context where the actual type of the Model unknown. For example, when recursively traversing a tree consisting of many different components with different models.


Alternative


If there is a possibility to render the interface of a component in a type allowing creation of a type-object, then it is better to do so, and instead of the component itself, operate it with a type-object:


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

Launch


Conclusion


It turns out that closures in Rust can capture not only objects of the environment, but also types. At the same time, they can be interpreted as ordinary functions. This property becomes useful when it is required to work in a uniform manner with different types without losing information about them if the types of objects are not applicable.


Hope this article helps you in using Rust. Share your thoughts in the comments.


')

Source: https://habr.com/ru/post/454310/


All Articles