Traits in Rust 2: Foreign Traits


Disclaimer: This section assumes you have some familiarity with Rust, and that you have already tried writing some code of your own.

If you haven't or if you are out of inspiration, try writing a grep clone:

  • program accepts two parameters, file path and string
  • the file in question is read line by line
  • print to standard output every line that contains string

Your program should gracefully handle errors and invalid input, ie. don't do things that can panic, don't use .unwrap() or .expect(). The grep clone should not crash if parameters are missing, file does not exist, or is unreadable as text.

Design your program in such a way that you can write some tests.

Prerequisites

In Part 1 of this chapter, we implemented a list. However, well written libraries do not have types that exist in a vacuum, we need to enhance our previous creation to ensure it integrates well with the rust standard library and, by extension, foreign code.

As we mentioned earlier, traits are the cornerstone of Rust development. It is only natural that many things which are in other languages represented by attributes, pragmas or type inheritance are represented by traits in Rust.

This also includes common syntactic sugars such as:

  • Indexing
  • Operator overloading
  • Deep copy-ing
  • Comparisons
  • Callability (think functors from C++)
  • Printability

And important type qualities and behaviors:

  • Conversions
  • Dereferencing
  • Copy vs Move semantics and destructors
  • Compatibility as an iterator and by extension usage in loops
  • Presence of default value
  • Thread-safety
  • Constant sizing (ie "Is this a statically or dynamically sized type?")

Some of these more low-level / intrinsic traits are implemented automatically by every type if applicable, and have a more descriptive nature useful for restricting which types can be put into which generic parameter place, and implementing them manually (which generally requires unsafe code if it is possible at all) will not alter the type's behavior in any way but may introduce the possibility of blowing your leg off in a spectacular manner.

Most notable of these auto-traits are Sized (this type is statically sized, TIP: pointer to every type is sized), Send (this type can be sent/moved to another thread), Sync (this type can be shared between threads).

#![allow(unused)]
fn main() {
type Priority = ();
type JobId = usize;
fn send_to_worker<T>(_: T, _: Priority) -> JobId { 0 }

trait Job {
    // trait for job objects which can be executed by worker threads
    fn is_completed(&self) -> bool;
    // other items here...
}

pub fn safety_first<T>(job: T, prio: Priority) -> Result<JobId, String>
where
    T: Job + Send + Sync
{
    if !job.is_completed() {
        let job_id = send_to_worker(job, prio);
        Ok(job_id)
    } else {
        // Please don't use strings as error in production 🙈
        Err("Cannot start a job which is already completed!".into())
    }
}
}

Derivable traits

To eliminate boilerplate and work needed to implement some of these traits on these types, it is possible to use the #[derive(...)] attribute on structures to make Rust create implementations for you automatically.

#![allow(unused)]

fn main() {
#[derive(Debug, Default, Eq, PartialEq, Ord, PartialOrd, Copy, Hash)]
struct MyUInt(usize);
}

By deriving all of these traits, MyUInt is now a tuple-struct type with a default value (MyUInt::default() == MyUInt(usize::default()) == MyUInt(0)), which can be printed with the debug format ("{:?}"), can be compared for equality and for order, and those comparisons are definitive, is copied automatically wherever required and can be used as an index for HashMap etc.

Implementing these by hand hand would probably add a couple dozen to over a hundred lines.

TIP: Only derive traits you need to make sure you don't let users do anything with your type that you don't want them to do. However, it is useful - for debugging reasons - to derive (or implement) Debug on every type.

NOTE: the struct ANewType(OtherType) is seen commonly Rust. Called the newtype pattern, it is a means to implement traits from Dependency A over types from Dependency B. You can't directly do a impl dep_a::Trait for dep_b::Type because that would violate orphan instance rules as elaborated on in chapter Advanced Rust traits.

Short foray into the world of macros

You might or might not be surprised to learn that the derives are actually macros in disguise.

If you are coming from languages other than C, C++, Nim, or Lisp, you may not be familiar with the concept of a macro. A macro is a form of meta-programming utilizing input mapping. In layman's terms, a macro transforms tokens into other tokens, that is, code into other code.

Macros work solely on the level of source code and are not aware about semantics of the code they process, this is what makes them different from for example generics, templates or reflection.

A macro may or may not parse the input it processes, and based on its input produces more source code. Nowadays, macros are used for things such as reducing boilerplate, implementing Domain-Specific-Languages (for example HTML templates), imitating functions, or adding new syntax to a language.

Rust has support for two types of macros:

  • declarative - these utilize token patterns and resemble a match. They are declared with the macro_rules! keyword and are considerably simpler.
  • procedural - these are either functions or programs which programmatically process streams of tokens, generating new token streams. They are far more powerful, at the cost of increased complexity

The community sometimes uses abbreviated terms decl macro and proc macro.

Furthermore, macros fall into three categories:

  • Aforementioned #[derive] macros
  • Attribute-like macros that define custom attributes on any item, written #[my_attribute]
  • Function-like macros that look like function calls but operate on tokens specified as their arguments, written my_macro!(), my_macro![] or my_macro!{}
// This is a simple macro named `say_hello`.
macro_rules! say_hello {
    // `()` indicates that the macro takes no argument.
    () => {
        // The macro will expand into the contents of this block.
        println!("Hello!");
    };
}

fn main() {
    // This call will expand into `println!("Hello");`
    say_hello!()
}

example taken from https://doc.rust-lang.org/rust-by-example/macros.html

NOTE: For function-like macros, brackets are freely interchangeable. Traditionally, one uses round brackets / parentheses () for macros that work like a function call, square brackets [] for macros that produce collections and curly brackets / braces {} for macros that produce items or introduce new syntax, but nothing is stopping you from doing something like this:

fn main() {
    let _ = vec!(0, 1, 2);
    println!["hello, {} and {}", "Bob", "Anne"];
    let _ = line! {}; // returns line number in file
}

The task: Integrating the previous project

Much like this chapter is a continuation of the first chapter about traits, this project builds on top of the previous one. If you haven't done it yet, you should, they are intrinsically linked and it shouldn't take too long.

This time, it will be your task to implement foreign traits.

Step 1: Iterator

The most handy thing we can do for starters is to implement the Iterator trait for our sequences.

Look at the trait here:

https://doc.rust-lang.org/std/iter/trait.Iterator.html

If you look closely, you see you only need to provide two things:

  • the Item associated type
  • the next() method

Implement Iterator for all Sequence types.

You can use the following helper struct and trait:

#![allow(unused)]
fn main() {
struct SequenceIter<'a, T: Sequence>(&'a mut T);

trait SequenceExt<'a>
where
    Self: Sequence,
{
    fn iter(&'a mut self) -> SequenceIter<'a, Self>
    where
        Self: Sized;
}
}

Step 2: Add

What if we wanted to combine sequences to create new sequences? Well, it would be cool if we could do that using regular operators, such as +.

Let's implement just that.

https://doc.rust-lang.org/std/ops/trait.Add.html

Implement this trait for any combination of Sequence types, such that:

  • Any two Sequences can be added
  • Adding two sequences will produce a sequence of type Combined, which returns elements from one sequence added up with elements from second sequence
  • Both sequences should be reset
  • Calling .reset() on the combined sequence should reset both sequences
  • If one sequence would start returning None, the combined Sequence should also return one

Depending on when you are writing this, you might have to add an Add implementation for each left-hand type (ie. "impl for Fibonacci + Any Sequence"). That would be five times (impl Add for Combined as well). The implementation of combined should be valid for any combination of Rhs, and the two sequences contained in the Combined.

End product

In the end you should be left with a well prepared project, that has the following:

  • documented code explaining your reasoning where it isn't self-evident
  • optionally tests
  • and an example or two where applicable
  • clean git history that does not contain fix-ups, merge commits or malformed/misformatted commits

Your Rust code should be formatted by rustfmt / cargo fmt and should produce no warnings when built. It should also work on stable Rust and follow the Braiins Standard