Crate caet

source ·
Expand description

A judge for cause-effect systems.

This crate allows you to simulate an external environment that interacts with your object OO system prototype. This is useful for validating your OO design.

How does it help me?

Writing a simulation implies writing code, but not real code that interacts with the actual system (and that would burden you with the details of the system’s implementation), but fake code that interacts with a fake system that you have designed to be as close to the real system as necessary.

So, after analyzing your problem, and applying certain design patterns to plan what code you will write, you can use this crate to test your idea by writing lightweight, throwaway, fake code that interacts with your fake system.

Unlike real code, it’s only supposed to take you one or two hours to flesh out an entire system. And, unlike real code, you can keep it as long as you’d like without worrying about keeping up with the changes in the real system.

Use cases

  • Discussion and reflection: You can use this crate to discuss or document your design.
  • Prototyping: You can use this crate to prototype your design.
  • Validation: You can use this crate to validate your design.
  • Mocking: You can use this crate to mock your design. However, you’ll also be mocking your implementation, which may or may not be what you want.

Benefits Profile

This crate satisfies this niche:

Moderate accuracy, moderate cost: You do need to write code, mostly throwaway code at that, so this is more expensive than validating your idea on paper, but it’s much cheaper than writing real code.

It’s often believed that the only way to accurately gauge the quality of a design is to write real code. As they say: you can’t know until you try. No! This crate is the counterexample to that belief. Save yourself the time and effort of getting stuck with the wrong design, or the pressure to turn your flimsy prototype (with tons of anomalies) as the real thing.

What this crate offers you: write a little code about a little part of your system, test your system using a strategic “god” (judge) that controls the universe. Then, see if your object keeps up with the universe. Also, try to get a feel for what it’s like to actually turn your architecture into code.

Was it too verbose? Was it too complicated? Did it introduce a fundamental anomaly in your system? Did you miss something important? Did you lose flexibility? Did you gain flexibility? What did you feel after getting a taste of your design? Did you like it?

You can answer these questions without writing real code, plus, discuss and document your assumptions about the universe and the object’s responsibilities.

How to use this crate to test your OO system or prototype

First, there’s a full example right here on this page. You might have to scroll to the bottom of the page to see it.

And, there’s also a more elaborate example in the lib.rs file of this crate. It’s called mod test_stack. It’s much bigger than the example on this page, but also comprehensive. It’s a simulation of a stack data structure. Look at the example and see if caet is a good fit for you.

Now, let’s get to the details.

This crate has exactly three public types:

  • trait Judge: A god-like entity that controls the universe and judges the object’s reactions.
  • enum Judgment: A judgment about an object’s reaction evaluated at a particular time. Also contains the next input to the object.
  • struct Outcome: The final result of a test.

Your job is to implement the trait Judge.

And, essentially, one public function:

  • judge: The entry point into this crate. This is a test runner. It takes the ownership of your judge implementation and your object, and runs the simulation. Once it’s finished, it returns an Outcome.

So, it’s actually pretty simple. You implement the trait Judge, you implement your object, then you test it by calling judge. That’s it.

First, a synopsis.

The caet crate is a flexible testing system.

Any judge and any object can work together as long as they agree on one thing: the “change” data type.

The “change” data type is the type of the object’s sensory experiences and reactions. In other words, it really defines (the alphabet of) the language of the universe.

That means, anyone that speaks the same language (i.e. uses the same “change” data type) can work together.

Judge

Start by implementing the trait Judge.

There are three associated types you must implement:

  • Change: A combined observation and reaction type.
  • Fault: In case a reaction is unacceptable, this type is used to report the reason.
  • Error: A judge can also hit an error, in which case this type is used to report the reason.

Though you should define all three, only the first one (Change) is relevant to the OO design.

The second and third types are essentially for error handling. It’s not super important. You can make them String if you want.

Then, you must implement the trait method Judge::next.

Before we discuss Judge::next, you should think of the simulation as some sort of game or battle.

The “god” (i.e. the judge) controls the universe and the object. The judge’s goal is to defeat the object by making it hit a fault (a rule-breaking or otherwise inappropriate reaction). Meanwhile, the object’s goal is to survive the universe by not hitting a fault, and continue until it outsmarts the judge (i.e., ends the simulation). (Of course, you control both, so you can make them as boring or as interesting as you want.)

So, the Judge::next method should be thought of as the judge’s next move.

Namely, it will read all the reactions of the object that hasn’t been judged yet, and then it will decide what to do next.

The judge controls the lifetime of the universe. It can end the simulation at any time. The judge is also responsible for judging the object’s reactions. Both of these responsibilities are combined in the return value of Judge::next:

  • If the judge decides the reaction is acceptable and still has a challenge for the object, it will return Ok(Judgment::Continue(next_input)) with whatever the next_input is.
  • If the judge decides the reaction is acceptable but has no more challenges for the object, it will return Ok(Judgment::Done) to end the simulation.
  • Lastly, if the judge decides the reaction is unacceptable, it will return Err(Judgment::Fault(fault)) with whatever the fault is. This also ends the simulation.

In Judge::next, you’ll be implementing the judge’s decision-making process.

Actually, this single method is the only thing you need to implement, and it conceptually defines the judge’s identity and the rules of the universe.

Your object

The definition of an object is any thing that reacts to changes in its environment.

Because the “thing” concept is so general, it would be unreasonable to require you to define it as some sort of a concrete data type. Instead, in this crate, your object will be represented by a closure, called its reaction function.

In the signature of judge, you must provide a closure that implements FnMut(J::Change) -> Vec<J::Change> where J is your implementation of Judge. This closure is your object’s reaction function, and, therefore, its representation.

Let’s break this down.

  • FnMut: The object has private state that it can mutate, which it persists while it is called multiple times.
  • (J::Change): It makes a passive observation of the universe, though it is actually given by the judge.
  • -> Vec<J::Change>: It reacts to the observation by producing a vector of changes. It’s a vector because it can produce multiple changes at once, or none at all. Pay close attention to the doctrine of non-immediacy of reactions: This doctrine says that, unlike observations, reactions are not immediate. In other words, it’s possible for the object to bunch up its reactions and produce them all at once as a way to defeat the judge. The judge should generally agree that this is a valid strategy. Otherwise, the judge is at fault. (But of course, this is up to how you implement your judge. I only strongly recommend that you follow this doctrine.)

So in summary, the “object” is abstracted away, hidden behind the FnMut closure. The caet crate will never ever touch it directly. Instead, you will be providing a closure that stands in for your object.

In more conventional OOP terms, the closure is a proxy, translating each “request” (or command or event) to your real object, and then translating each “response” (or reaction) back to the judge.

But, your proxy can actually get a bit smarter than the underlying object, by bunching up reactions and releasing them all at once perhaps in unpredictable ways. Maybe it will even drop or mutate certain reactions and re-order the reactions. The precise rules for what’s allowed and not, and how synchronization is done, should be agreed upon between the judge and the object. This means, it’s your job to define how reliable your object’s reactions are, whether they are immediate or not, and, in general, how time is understood in your universe.

However, the doctrine of the non-immediacy of reactions, and that of the non-reliability of reactions, can provide a good starting point.

(But none of the examples given respect them fully. But, they are just examples, anyway.)

judge (lowercase)

The function judge takes in exactly two arguments:

  • judge: Your implementation of Judge.
  • object: Your object. A function closure as described above.

It returns an Outcome type if the judge successfully completes, or Judge::Error, an Judge-associated type, if the judge fails to simulate the universe.

Once an Outcome is produced, you can inspect it to see whether the object has passed or failed the test, and the number of observations (calls field) made by the object.

Don’t forget the check to see if the object passed too early test because, while the object’s reactions were acceptable, they went beyond what the judge was expecting, which caused it to prematurely halt the test. That’s why the Outcome type has a iteration count field. Check that field to see if it’s way too low.

Some doctrines that may help

Let’s first refine the concepts of observations and reactions. They are primitive notions so they can’t really be defined, but at least, we can say what they are not.

Observations

  • Passive: The object senses things passively. It can’t choose to sense something or not, neither can it arrange some signal to arrive at a certain time. In caet, this effect is achieved by letting the judge control all sensations (observations) felt by the object.
  • Immediate: The object’s observations are immediate. They are always up-to-date. This also means they always arrive in order. In fact, a “delayed sensation” (or observation) is a contradiction in terms; like a triangle with four corners, it cannot even be imagined.
  • Reliable: Similarly, all observations “felt” by the object are reliable. Again, this is by definition of the very term “observation.” Implementation-wise, this means caet cannot drop or mutate observations sent by the judge.

Reactions

  • Active: The object has total and unimpeachable agency over its reactions. Of course, some actions can be disallowed by the judge, but in any case, the object will always be looking out for its own interests.
  • Non-immediate: In the real world, reactions are not immediate. They can also be re-ordered.
  • Non-reliable: Similarly, reactions are not reliable. They can be dropped or mutated.

However, caet doesn’t enforce the non-immediacy and non-reliability doctrines, meaning, a judge implementation may actually demand immediate and reliable reactions.

When implementing these doctrines, you do this by letting the reaction function, which is a proxy for your object, control the way reactions are sent over the Vec<J::Change> vector. For example, you can bunch up reactions and release them all at once, or you can drop or mutate certain reactions, or you can re-order them.

The judge will not know about this, and will only see what’s sent over the “wire” (the vector). It’s up to you to make sure the judge understands your object’s reaction strategy.

The doctrine of immediacy of observations

The object’s observations are immediate. They are always up-to-date. This also means they always arrive in order.

This is a non-negotiable doctrine. caet enforces it.

The doctrine of reliability of observations

Similarly, all observations “felt” by the object are reliable. This means caet cannot drop or mutate observations sent from the judge.

Again, this is a non-negotiable doctrine. caet also enforces it.

The optional doctrine of non-immediacy of reactions

The object’s reactions are not immediate. They can be bunched up and released all at once. This is a valid strategy, and the judge should agree to it. Otherwise, the judge is at fault.

Philosophically, this is an absolute one, but caet doesn’t rely on it. It’s really up to your judge implementation to either expect immediate reactions or not.

What this means is: caet doesn’t inspect the vector of reactions returned by the object. It just passes it along to the judge. But you can definitely simulate the non-immediacy by buffering the reactions in your implementation of the reaction function.

The optional doctrine of non-reliability of reactions

The object’s reactions are not reliable. They can be dropped or mutated. This is a valid strategy, and the judge should agree to it. Otherwise, the judge is at fault.

Again, you can say yes or no to this doctrine. caet doesn’t touch anything, so it’s up to you.

Example

The source code provides a more detailed example in the module test_stack.

But, here, let’s do a much simpler one that concisely demonstrates the basic idea of the crate.

We model an agent that plays the guessing game.

The range of possible numbers is -1000 to 1000, and the agent must guess the correct number within 10 guesses.

In this code,

  1. We define the universe.
  2. We define the judge. (see Judge)
  3. We define the object.
  4. We test the object with the judge. (see judge)

In four steps, we have a working simulation. Now, I can be very sure of the pros and cons (not shown) of my architecture, making me more confident in my design decisions.

use caet::{judge, Judge, Judgment, Outcome};
use std::convert::Infallible;

/// Universe
#[derive(Debug, PartialEq)]
enum GuessIs {
    Start, // The initial call to `next` is always with an empty vector
    TooLow, // Judge: The agent's guess was too low
    TooHigh, // Judge: The agent's guess was too high
    Value(i32), // Agent: Here's my guess
}
use GuessIs::*;

/// Judge
struct MyJudge {
    count: u32,
    target: i32,
    begun: bool,
}
impl Judge for MyJudge {
    type Change = GuessIs; // All changes in the universe
    type Fault = String; // What it means for a reaction to be unacceptable
    type Error = Infallible; // What it means for the judge to fail
    /// Get object's reactions, judge them,
    /// and, if acceptable, return the next input or stop.
    fn next(&mut self, reactions: Vec<GuessIs>) -> Result<Judgment<GuessIs, String>, Infallible> {
        // The initial call to `next` is always with an empty vector, given by the `judge`
        // procedure itself, so prepare for that.
        if !self.begun {
            self.begun = true;
            return Ok(Judgment::Continue(Start));
        }

        // Now, all subsequent calls to `next` has a vector that was created by the object.

        // Ignore earlier reactions, if many
        println!("reactions: {:?}", reactions);
        if let Some(reaction) = reactions.last() {
            self.count += 1;
            if self.count > 10 {
                // Unacceptable: Too many guesses
                return Ok(Judgment::Fault(format!("It was {}.", self.target)));
            }
            match reaction {
                // Acceptable: Legitimate actions by the agent
                Value(n) if *n == self.target => return Ok(Judgment::Done),
                Value(n) if *n < self.target => return Ok(Judgment::Continue(TooLow)),
                Value(n) if *n > self.target => return Ok(Judgment::Continue(TooHigh)),
                // Unacceptable: Faulty implementation of the agent
                _ => return Ok(Judgment::Fault(
                    format!("Invalid reaction type for agent: {:?}",
                    reaction
                ))),
            }
        }

        // In reality, according to the doctrine of non-immediacy of reactions,
        // the object can bunch up its reactions and produce them all at once,
        // which is, by the doctrine, a valid strategy. If I followed this
        // doctrine, then this judge is actually incorrect, because it's
        // expecting the object to react immediately. But, I'm not following
        // the doctrine for the sake of simplicity.

        // (Of course, the doctrine does allow for the possibility of adding
        // synchronization primitives to the universe, after which
        // the object *must* react "soon," or the judge will keep waiting
        // until it's waited "too long" and fails the object. But that's
        // not relevant here.)

        Ok(Judgment::Fault(format!("You can't just pass a turn.")))
    }
}

// My object (bisect)
let mut lower_bound = -1000;
let mut upper_bound = 1000;
let mut guess = 0;
// (Proxy for my object)
let mut proxy = |observation| {
    println!("I sensed: {observation:?}");
    match observation {
        Start => {
            guess = (lower_bound + upper_bound) / 2;
            vec![Value(guess)]
        }
        TooLow => {
            lower_bound = guess;
            guess = (lower_bound + upper_bound) / 2;
            vec![Value(guess)]
        }
        TooHigh => {
            upper_bound = guess;
            guess = (lower_bound + upper_bound) / 2;
            vec![Value(guess)]
        }
        Value(_) => panic!("You're not supposed to guess!"),
    }
};

// Test
let outcome: Outcome<MyJudge> = judge(
    MyJudge { count: 0, target: 42, begun: false },
    &mut proxy,
).unwrap();
assert_eq!(outcome.judgment, Judgment::Done);
println!("It took {} guesses.", outcome.calls);

Structs

  • The final judgment of a cause-effect system.

Enums

  • A judgment of a cause-effect system.

Traits

  • A judge for a cause-effect system.

Functions

  • A test driver for a cause-effect system.
  • Like judge, but panic on any error, either due to the judge or the task.