Posted on by & filed under Content - Highlights and Reviews, Programming & Development.

codeA guest post by Jeffery Olson, a software engineer currently living and working in the Seattle area. He’s still searching for that sweet spot between pragmatism and idealism. Besides contributions within the Rust community, he is interested in building performant HTML5 applications using emerging web technologies like WebGL and WebRTC. He can be reached at @olsonjeffery.

In my previous posts on Rust, the focus centered on using Rust’s Runtime API to perform I/O (with files and networking, respectively). For this post, I will cover two of the major strengths of the language: generics and constraints.

When writing Rust code, you may find yourself in a situation where you want to create a type/function/method whose input(s) should be restricted to only those that implement some given traits. This is a common use case in generic/parameterized programming. In Rust, this is referred to as applying restrictions or bounds on type parameters.

Consider the following:

The Image trait represents some underlying bitmap with a method, get_pixels, returning a slice of raw 32bit color definitions. There’s a Sprite struct, which contains a single field, bitmap. The Sprite encapsulates high-level, logical operations to be carried out on its bitmap. We wish to restrict Sprite instances to contain values that satisfy the Image trait. This provides the benefit of allowing us to assume that bitmap will, at a minimum, satisfy the API declared in Image.

The curious thing, here, is that Rust’s generic constraints are always declared in the context of invocation on the type, and not in the type declaration itself. That is: bounds are always placed within functions, and methods or impl blocks (in the latter case they apply to all methods within the impl). Some explanation for this rationale is in this blog post by Rust team member Niko Matsakis (in the section titled “Bounds in type definitions”).

With this in mind, there are a number of approaches to accomplish our above-stated goal (limiting Sprite<T> to only contain types that implement Image). One approach would be to eschew bounds altogether and implement Sprite in terms of a boxed trait object, like so:

While fulfilling our goal, this has the drawback of, more or less, defeating the purpose of using generics in the first place. ~Image is awkward to use in practice (all values that implement Image will have to be boxed on the heap). Furthermore, this approach has a performance penalty. Trait objects (as ~Image or &Image values are known) use dynamic dispatch and vtables.

A more idiomatic approach (the above example is suboptimal straw code) would be to put a bound on Sprite‘s impl block:

The difference is: a bound is placed by the <T: Image> annotation after the impl keyword. This states “all uses of T in the following impl block must abide by this constraint,” since the T alias is used in Sprite<T>. At build-time, the Rust compiler can statically guarantee these constraints are honored and generate code that uses more efficient static dispatch.

This also means that methods within the impl can interact with the bitmap field under the assumption that Image‘s methods can be called on it:

Bounds can also be placed on individual, freestanding functions:

The key thing to understand is that individual methods/functions declare generic parameters and trait restrictions simultaneously. impl blocks, on the other hand, declare their restrictions at the beginning of the block. Any type bounds declared (e.g. the T in <T:Image>) are applied at the declaration of the type the impl concerns (Sprite<T> in the above examples). Those restrictions affect all uses of the parameter within the impl block (it’s like every method within the impl has <T: Image> applied to it, in addition to any other bounds/parameters they declare).

Any function/method can be parameterized, regardless of the underlying type. Be mindful that declaring a parameter on a method implementation with the same identifier as a paremeter on its parent impl will have the effect of shadowing the impl‘s parameter (possible with the common re-use of T).

For example:

This will result in a compiler error:

Here is yet another variation on our original goal:

Here, Sprite is no longer parameterized at all. The parameterization is restricted to the new static method, which takes any value whose type implements Image, boxes it and stores it within the Sprite. This has the drawbacks listed above with trait objects. Trait objects tend to be most useful in situations where you must handle the heterogeneous collection of values that implement a common trait, and should otherwise be avoided.

As an aside: in order to box input, its type must also implement Send, which is a special compiler-inferred Trait for those values that can be safely packaged and passed across task boundaries (it is required for all types that can be stored in a ~ box). Multiple Trait restrictions can be chained with the + operator.

Conclusion

This is just the tip of the iceberg, when it comes to parameterization and generics in Rust.
For more information, you should take a look at the Rust Manual.

For more details about general programming languages, see the Safari Books Online resources referenced below.

Not a subscriber? Sign up for a free trial.

Safari Books Online has the content you need

Tags: Bounds, Constraints, Generics, Parameters, Rust,

Comments are closed.