moodmosaic

The Expression Problem, with Counters

This post is part of the Model-Based Stateful Testing with madhouse-rs series.

The expression problem is a programming puzzle: How do you design code so you can add new data types and add new operations without rewriting everything?

This tension isn’t just theoretical. It shows up directly in model-based stateful testing (MBT) frameworks. Let’s start simple: with a simple counter as an example.

Data closed, operations open (enum style)

#[derive(Debug, Clone)]
// An `enum` defines a type that can be one of a few variants.
enum Counter {
    Simple(i32),
    Bounded { value: i32, max: i32 },
}

fn inc(c: &mut Counter) {
    match c {
        Counter::Simple(v) => *v += 1,
        Counter::Bounded { value, max } => {
            if *value < *max { *value += 1 }
        }
    }
}

fn dec(c: &mut Counter) {
    match c {
        Counter::Simple(v) => *v -= 1,
        Counter::Bounded { value, .. } => *value -= 1,
    }
}

Adding a new operation (like reset) is easy: just write another fn. But adding a new variant forces edits in every function.

Data open, operations closed (trait style)

// A `trait` defines a set of methods that a type must implement.
trait Counter {
    fn new() -> Self;
    fn inc(&mut self);
    fn dec(&mut self);
    fn get(&self) -> i32;
}

struct Simple(i32);
impl Counter for Simple {
    fn new() -> Self { Simple(0) }
    fn inc(&mut self) { self.0 += 1 }
    fn dec(&mut self) { self.0 -= 1 }
    fn get(&self) -> i32 { self.0 }
}

struct Bounded { value: i32, max: i32 }
impl Counter for Bounded {
    fn new() -> Self { Bounded { value: 0, max: 0 } }
    fn inc(&mut self) {
        if self.value < self.max { self.value += 1 }
    }
    fn dec(&mut self) { self.value -= 1 }
    fn get(&self) -> i32 { self.value }
}

Here, new data types are easy (implement the trait), but new operations require editing the trait and every implementation.

Why this matters for MBT

This is exactly the same trade-off MBT frameworks face:

We’ll see this trade-off play out with two MBT frameworks in Rust: proptest-state-machine and madhouse-rs.


Next: Model-Based Stateful Testing with proptest-state-machine