Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/book/models/disease_model/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ publish = false # Do not publish to the Crates.io registry

[dependencies]
csv = "1.3.1"
ixa = { git = "https://github.com/CDCgov/ixa", branch = "main" }
# TODO: revert to git dependency before merging
ixa = { path = "../../../.." }
rand_distr = "0.5.1"
serde = { version = "1.0.217", features = ["derive"] }
2 changes: 1 addition & 1 deletion docs/book/models/disease_model/src/people.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ define_property!(
pub fn init(context: &mut Context) {
trace!("Initializing people");
for _ in 0..POPULATION {
let _: PersonId = context.add_entity(()).expect("failed to add person");
let _ = context.add_entity(Person).expect("failed to add person");
}
}
// ANCHOR_END: init
4 changes: 2 additions & 2 deletions docs/book/models/disease_model/src/transmission_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use ixa::prelude::*;
use ixa::trace;
use rand_distr::Exp;

use crate::people::{InfectionStatus, PersonId};
use crate::people::{InfectionStatus, Person};
use crate::{FORCE_OF_INFECTION, POPULATION};

define_rng!(TransmissionRng);
Expand All @@ -12,7 +12,7 @@ define_rng!(TransmissionRng);
// ANCHOR: attempt_infection
fn attempt_infection(context: &mut Context) {
trace!("Attempting infection");
let person_to_infect: PersonId = context.sample_entity(TransmissionRng, ()).unwrap();
let person_to_infect = context.sample_entity(TransmissionRng, Person).unwrap();
let person_status: InfectionStatus = context.get_property(person_to_infect);

if person_status == InfectionStatus::S {
Expand Down
134 changes: 4 additions & 130 deletions docs/book/src/appendix_rust/turbo-fish.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,132 +6,7 @@ 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:

```rust
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:

```rust
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:

```rust
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:

```rust
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):

```rust
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:

```rust
// 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:

```rust
// 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:

```rust
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
## 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
Expand All @@ -146,14 +21,13 @@ let status: InfectionStatus = context.get_property(person_id);
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:
The generic types for querying and sampling
methods can usually be inferred by the compiler:

```rust
// A silly example, but no turbo fish is required.
context.with_query_results(
(Age(30), Alive(true)),
with!(Person, Age(30), Alive(true)),
|people_set| println("{:?}", people_set)
);
```
Expand Down
34 changes: 15 additions & 19 deletions docs/book/src/first_model/people.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,29 +57,25 @@ pub fn init(context: &mut Context) {
trace!("Initializing people");

for _ in 0..100 {
let _: PersonId = context.add_entity(()).expect("failed to add person");
let _ = context.add_entity(Person).expect("failed to add person");
}
}
```

The `context.add_entity()` method call might look a little odd, because we are
not giving `context` any data to insert, but that is because our one and only
`Property` was defined to have a default value of `InfectionStatus::S`
(susceptible)—so `context.add_entity()` doesn't need any information to create a
new person. Another odd thing is the `.expect("failed to add person")` method
call. In more complicated scenarios adding a person can fail. We could intercept
that failure if we wanted, but in this simple case we will just let the program
crash with a message about the reason: "failed to add person".

Finally, the `Context::add_entity` method returns an entity ID wrapped in a
`Result`, which the `expect` method unwraps. We can use this ID if we need to
refer to this newly created person. Since we don't need it, we assign the value
to the special "don't care" variable `_` (underscore), which just throws the
value away. Why assign it to anything, though? So that the compiler can infer
that it is a `Person` we are creating, as opposed to some other entity we may
have defined. If we just omitted the `let _: PersonId =` part completely, we
would need to explicitly specify the entity type using
[turbo fish notation](../appendix_rust/turbo-fish.md).
We use `Person` here to represent a new entity with all default property values–
our one and only `Property` was defined to have a default value of
`InfectionStatus::S` (susceptible), so no additional information is needed.

The `.expect("failed to add person")` method call handles the case where adding
a person could fail. We could intercept that failure if we wanted, but in this
simple case we will just let the program crash with a message about the reason:
"failed to add person".

The `Context::add_entity` method returns an entity ID wrapped in a `Result`,
which the `expect` method unwraps. We can use this ID if we need to refer to
this newly created person. Since we don't need it, we assign the value to the
special "don't care" variable `_` (underscore), which just throws the value
away.

## Constants

Expand Down
14 changes: 8 additions & 6 deletions docs/book/src/first_model/transmission.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,14 @@ The function `attempt_infection()` needs to do the following:
Read through this implementation and make sure you understand how it
accomplishes the three tasks above. A few observations:

- The method call `context.sample_entity(TransmissionRng, ())` takes the name of
a random number source and a query and returns an `Option\<PersonId>`, which
can have the value of `Some(PersonId)` or `None`. In this case, we give it the
"empty query" `()`, which means we want to sample from the entire population.
The population will never be empty, so the result will never be `None`, and so
we just call `unwrap()` on the `Some(PersonId)` value to get the `PersonId`.
- The method call `context.sample_entity(TransmissionRng, Person)` takes the
name of a random number source and a query and returns an `Option\<PersonId>`,
which can have the value of `Some(PersonId)` or `None`. In this case, we use
`Person` and no property filters, which means we want to sample from the
entire population. If we wanted to, we could pass filters with the `with!` macro
(e.g., `with!(Person, Region("California"))`) The population will
never be empty, so the result will never be `None`, and so we just call
`unwrap()` on the `Some(PersonId)` value to get the `PersonId`.
- If the sampled person is not susceptible, then the only thing this function
does is schedule the next attempt at infection.
- The time at which the next attempt is scheduled is sampled randomly from the
Expand Down
45 changes: 14 additions & 31 deletions docs/book/src/migration_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,50 +126,33 @@ These macros automatically create a type alias of the form

### Adding a new entity (e.g. a new person)

Adding a new entity with multiple property values:
Adding a new entity with only default property values by passing the entity type
directly:

```rust
// Assuming the `Person` entity is defined somewhere.
// Add a new entity (a person in this case) to an existing `Context` instance we have access to.
let person_id = context.add_entity((Age(25), InfectionStatus::Infected)).unwrap();
let person_id = context.add_entity(Person).unwrap();
```

Observe:

- The compiler is smart enough to know that we are adding a new `Person` entity
because we supplied a list of property values that are properties for a
`Person`.
- The `add_entity` function takes a "property list", which is just a tuple of
property values. The properties must be distinct, of course, and there must be
a value for every "required" property, that is, for every (non-derived)
property that doesn't have a default value.
- A single-property tuple uses the syntax `(Age(25), )`. Notice the awkward
trailing comma, which lets the compiler know the parentheses are defining a
tuple rather than functioning as grouping an expression.
(This example assumes there are no required properties, that is, that every
property has a default value.)

Adding a new entity with just one property value:
Adding a new entity with property values using the `with!` macro:

```rust
// Assuming the `Person` entity is defined somewhere.
// Add a new entity (a person in this case) to an existing `Context` instance we have access to.
let person_id = context.add_entity((Age(25), )).unwrap();
let person_id = context.add_entity(with!(Person, Age(25), InfectionStatus::Infected)).unwrap();
```

Adding a new entity with only default property values:
The `with!` macro takes the entity type as its first argument, followed by any
property values. The properties must be distinct, of course, and there must be
a value for every "required" property, that is, for every (non-derived)
property that doesn't have a default value.

```rust
// If you specify the `EntityId\<E>` return type, the compiler uses it to infer which entity to add.
// This is a good practice and avoids the special "turbo fish" syntax.
let person_id: PersonId = context.add_entity(()).unwrap();
Adding a new entity with just one property value:

// If we don't specify the `EntityId\<E>` type, we have to explicitly tell the compiler *which* entity
// type we are adding, as there is nothing from which to infer the entity type.
let person_id = context.add_entity::<Person, _>(()).unwrap();
```rust
let person_id = context.add_entity(with!(Person, Age(25))).unwrap();
```

(These two examples assume there are no required properties, that is, that every
property has a default value.)

### Getting a property value for an entity

```rust
Expand Down
3 changes: 2 additions & 1 deletion docs/book/src/topics/indexing.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,8 @@ Suppose we have the properties `AgeGroup` and `InfectionStatus`, and we want to
speed up queries of these two properties:

```rust
let age_and_status = context.query_result_iterator((AgeGroup(30), InfectionStatus::Susceptible)); // Bottleneck
let query = with!(Person, AgeGroup(30), InfectionStatus::Susceptible);
let age_and_status = context.query_result_iterator(query); // Bottleneck
```

We could index `AgeGroup` and `InfectionStatus` individually, but in this case
Expand Down
2 changes: 1 addition & 1 deletion examples/basic-infection/src/people.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ define_property!(
pub fn init(context: &mut Context) {
trace!("Initializing people");
for _ in 0..POPULATION {
let _: PersonId = context.add_entity(()).unwrap();
let _ = context.add_entity(Person).unwrap();
}
}
6 changes: 3 additions & 3 deletions examples/basic-infection/src/transmission_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ use ixa::prelude::*;
use ixa::trace;
use rand_distr::Exp;

use crate::people::{InfectionStatus, Person, PersonId};
use crate::people::{InfectionStatus, Person};
use crate::{FOI, MAX_TIME};

define_rng!(TransmissionRng);

fn attempt_infection(context: &mut Context) {
trace!("Attempting infection");
let population_size: usize = context.get_entity_count::<Person>();
let person_to_infect: PersonId = context.sample_entity(TransmissionRng, ()).unwrap();
let person_to_infect = context.sample_entity(TransmissionRng, Person).unwrap();

let person_status: InfectionStatus = context.get_property(person_to_infect);

Expand Down Expand Up @@ -52,7 +52,7 @@ mod test {
fn test_attempt_infection() {
let mut context = Context::new();
context.init_random(SEED);
let person_id: PersonId = context.add_entity(()).unwrap();
let person_id = context.add_entity(Person).unwrap();
attempt_infection(&mut context);
let person_status: InfectionStatus = context.get_property(person_id);
assert_eq!(person_status, InfectionStatus::I);
Expand Down
4 changes: 2 additions & 2 deletions examples/births-deaths/src/infection_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ mod test {

let population_size: usize = 10;
for index in 0..population_size {
let person = context.add_entity((Age(0),)).unwrap();
let person = context.add_entity(with!(Person, Age(0))).unwrap();

context.add_plan(1.0, move |context| {
context.set_property(person, InfectionStatus::I);
Expand All @@ -168,7 +168,7 @@ mod test {
context.init_random(42);
init(&mut context);

let person = context.add_entity((Age(0),)).unwrap();
let person = context.add_entity(with!(Person, Age(0))).unwrap();
context.add_plan(1.1, move |context| {
cancel_recovery_plans(context, person);
});
Expand Down
Loading