Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[book] document example of manual arbitrary impl #514

Merged
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
83 changes: 82 additions & 1 deletion book/src/proptest/tutorial/arbitrary.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,86 @@ In some cases, where it makes sense to define a canonical strategy, such as in
the [JSON AST example](recursive.md), it is a good idea to implement
`Arbitrary`.


## Deriving `Arbitrary`

The experimental [`proptest-derive` crate](../../proptest-derive/index.md) can
be used to automate implementing `Arbitrary` in common cases.
be used to automate implementing `Arbitrary` in common cases. For example, imagine we have a struct that represents a point in a 2-D coordinate space:
```rust
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
```
This struct has the property that any pair of valid `i32`s can make a valid `Point`, so that is perfect for using `#[derive(Arbitrary)]`.

## Manual `Arbitrary` implementations

Sometimes, however, there are extra constraints that your type has, which the derive macro can't understand. In these cases, you'll need to implement `Arbitrary` for your type manually.

For example, consider this struct which represents a range (note, the derive API is can actually represent this case, it's just an example):
```rust
#[derive(Debug)]
struct Range {
lower: i32,
upper: i32,
}

impl Range {
pub fn new(lower: i32, upper: i32) -> Option<Self> {
if lower <= upper {
Some(Self { lower, upper })
} else {
None
}
}
}
```
This struct has an invariant: `lower <= upper`. However, if we derive an `Arbitrary` implementation naively, it might generate `Range { lower: 1, upper: 0 }`.

Instead, we can write a manual implementation:
```rust
impl Arbitrary for Range {
type Parameters = ();
type Strategy = FilterMap<StrategyFor<(i32, i32)>, fn((i32, i32)) -> Option<Self>>;

fn arbitrary_with(_parameters: Self::Parameters) -> Self::Strategy {
any::<(i32, i32)>() // generate 2 arbitrary i32s
.prop_map(|(a, b)| {
let (lower, upper) = if a < b {
(a, b)
} else {
(b, a)
};
Range::new(lower, upper).unwrap()
})
}
}
```
Here, there are three items we need to define:
- `type Parameters` - the type of any parameters to `arbitrary_with`. Here (and in many cases), we don't need this, so `()` is used.
- `type Strategy` - the type of the strategy produced
- `fn arbitrary_with` - the code that creates the canonical `Strategy` for this type

It's important to consider what type you want to use for `Strategy`. Here, we explicitly write the type out. This uses static dispatch, which is often faster and easier to optimize, but has a few downsides:
- you need to write out the type of the strategy. Even for this small function, it's a pretty lengthy function signature. In the worst case, it's impossible, since some types are unnameable (e.g. closures which capture their environment)
- it makes the implementation of `arbitrary_with` a part of your public API signature (if you expose `Arbitrary` impls in general from your crate). This means that changes to the implementation may require a breaking change.

There are a couple of ways around this:
- heap-allocate the strategy by:
- returning `BoxedStrategy<T>`
- calling `.boxed()` on the strategy before returning it
- use the nightly-only `#![feature(type_alias_impl_trait)]`:
```rust
type RangeStrategy = impl Strategy<Value = Range>;

impl Arbitrary for Range {
type Parameters = ();
type Strategy = RangeStrategy;
// ...
}
```

Using `BoxedStrategy` will incur some performance penalty relating to a heap allocation as well as dynamic dispatch, but it works on stable (as of November 2022).

Loading