Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Specifying Generic Types and the Turbo Fish

Rust and ixa in particular make heavy use of type generics, a way of writing a single piece of code, a function for example, for a whole family of types at once. A nice feature of Rust is that the compiler can often infer the generic types at the point the function is used instead of relying on the programmer to specify the types explicitly.

An example with Context::add_entity()

Suppose we want to initialize a population:

define_entity!(Person);
define_property!(
    // The type of the property
    enum InfectionStatus {S,I,R},
    // The entity the property is associated with
    Person,
    // The property's default value for newly created `Person` entities
    default_const = InfectionStatus::S
);

/// Populates the "world" with people.
pub fn init(context: &mut Context) {
    for _ in 0..1000 {
        context.add_entity((InfectionStatus::S, )).expect("failed to add person");
    }
}

During the initialization of our population, we explicitly told ixa to create a new susceptible person, that is, with the InfectionStatus::S property value. However, when we defined the InfectionStatus property with the define_property! macro, we specified a default initial value with default_const = InfectionStatus::S. Since InfectionStatus has a default value, we don't need to supply a value for it when calling context.add_entity(...). But remember, the compiler infers which entity to create based on the property values we supply, and if we don't supply any property values, we need another way to specify the entity to create.

The Context::add_entity function is actually a whole family of functions Context::add_entity<E: Entity, PL: PropertyList>, one function for each concrete Entity type E and PropertyList type PL. When we call context.add_entity(...) in our code with a tuple of properties (the initialization list), the Rust compiler looks at the initialization list and uses it to infer the concrete types E and PL. But when the initialization list is PL=() (the empty list), the compiler doesn't know what the Entity type E should be. The Rust language allows us to tell it explicitly using the "turbo fish" notation:

context.add_entity::<Person, ()>(()).expect("failed to add person");

Actually, the compiler always already knows the PropertyList type PL, so we can use the "wildcard" _ (underscore) to tell the compiler to infer that type itself:

context.add_entity::<Person, _>(()).expect("failed to add person");

There is another way to give the compiler enough information to infer the Entity type, namely by specifying the return type we are expecting. In our case, we just throw the returned PersonId away, but suppose we want to refer to the newly created person. We could write:

let person_id: PersonId = context.add_entity(()).expect("failed to add person");

The Entity type E must be Person, because that is the only way Context::add_entity<E: Entity, PL: PropertyList> can return a PersonId (a type alias for EntityId<Person>). You can use this trick even if you never use the returned PersonId, but in such a case it's best practice to signal this intent by using the special "don't care" variable _ (underscore):

let _: PersonId = context.add_entity(()).expect("failed to add person");

You do not have to learn the rules for when specifying the types using turbo fish notation is required. The compiler will let you know. For add_entity, always specifying the returned type means you'll never have to worry about turbo fish notation.

Preferred Idiom for Context::sample_entity()

The Context::sample_entity() method especially deserves discussion, because we often want to immediately use the returned value. If we try to use the standard Rust idiom to express this, we have to specify the types using turbo fish, which is awkward and ugly:

// Sample from the entire population by supplying the "empty" query. The last two `_`s are for the query type and
// RNG type, both of which the compiler can infer.
if let Some(person_id) = context.sample_entity::<Person, _, _>(TransmissionRng, ()) {
    // Do something with `person_id`...
}

Since we are sampling from the entire population, if sample_entity returns None, then the population is empty, and we clearly have a bug in our code, in which case the best thing to do is to crash the program and fix the bug. Thus, instead of the if let Some(...) = construct, it's actually better to just call unwrap on the returned value in this case. Here is a much more readable and simple way to write the code:

// Sample from the entire population by supplying the "empty" query. The compiler infers which entity to sample
// from the type of the variable we assign to.
let person_id: PersonId = context.sample_entity(()).unwrap();
// Do something with `person_id`...

If you really want to check for the None case in your code, assign the return value to a variable of type Option<PersonId> instead of immediately unwrapping the PersonId value. Then you can use if let Some(...) = or a match statement at your preference:

let maybe_person_id: Option<PersonId> = context.sample_entity(());
match maybe_person_id {
    Some(person_id) => {
        // Do something with `person_id`
    }
    None => {
        // Handle the empty population case
    }
}

Other Examples

The compiler's ability to infer the types of generic functions means that for most of the common functions the types do not need to be specified with turbo fish notation:

// The compiler "magically" knows to use the `get_property` method that fetches
// `InfectionStatus` because of the type of the variable being assigned to.
let status: InfectionStatus = context.get_property(person_id);
// Explicit types are almost never required for `Context::set_property`, because
// the types of the entity ID and property value are almost always already known.
context.set_property(other_person_id, status);

As with Context::add_entity, the generic types for querying and sampling methods can usually be inferred by the compiler except when the "empty" query is provided:

// A silly example, but no turbo fish is required.
context.with_query_results(
    (Age(30), Alive(true)),
    |people_set| println("{:?}", people_set)
);

A few methods always require the user to specify the generic type when they are called:

// Which entity are we counting? Here the return type is always `usize`.
let population: usize = context.get_entity_count::<Person>();
// Specify which property of which entity you'd like to index.
context.index_property::<Person, Age>();
// Specify the report to add.
context.add_report::<IncidenceReportItem>("incidence")?;
// Specify the event to subscribe to.
context.subscribe_to_event::<InfectionStatusEvent>(handle_infection_status_change);

In the last example above, the concrete type we specify is actually a type alias:

pub type InfectionStatusEvent = PropertyChangeEvent<Person, InfectionStatus>;

While it is not strictly necessary to define this type alias, you can see that the notation gets rather gnarly without it:


context.subscribe_to_event::<PropertyChangeEvent<Person, InfectionStatus>>(handle_infection_status_change);