microstates icon indicating copy to clipboard operation
microstates copied to clipboard

Guards or how to fail a transition

Open jeffutter opened this issue 5 years ago • 3 comments

In traditional 'state machines' usually, there is a way to disallow a transition. Perhaps based on business rules, consider the example of a cash register state machine.

You may have a cash register that starts with $100 in it. You may have a 'withdraw' action that lets you take money out. How would you prevent the following scenario:

CashRegister
  .balance.withdraw(20)
  .balance.withdraw(20)
  .balance.withdraw(100);

How do you handle this with microstates? I can possibly think of a couple solutions.

1.) Throw an error. I know it's often not recommended to throw unless it is a truly unexpected situation. 2.) Set some other sub-state to invalid. After every withdrawal, you could check something like updatedRegister.valid.state. Internally the 'withdraw' action would update the 'valid' sub-state when you overdraw.

Is there a more micro-statey way to address this?

jeffutter avatar Oct 31 '18 17:10 jeffutter

Hi @jeffutter,

That's a good question. I would probably do something like this,

class Account {
  amount = Number;
  withdraw(value) {
    return this.amount.decrement(value);
  }
}

class Error {
   message = String;
}

class CashRegister {
  balance = Account
  error = Error
  restrictedWithdrawl(amount) {
    if (this.balance.amount.state < amount) {
      return this.error.message.set(`You don't have enough money for this`);
    } else {
      return this.balance.withdraw(amount);
    }
  }
}

There are a few things to consider here,

  1. An account doesn't have any restrictions on it by default. These restrictions exists within the context of a CashRegister. The microstate shows this relationship.
  2. When you make a withdrawal, you perform an operation on the account within the context of the CashRegister, so the transition to perform the withdrawal is on the CashRegister
  3. Your input should be bound to the CashRegister and invoke restrictedWithdrawl transition

What do you think about this solution?

taras avatar Oct 31 '18 19:10 taras

Throwing an error would be one way. You could also have a computed property (which is kinda like your "valid.state")

class Balance {
  get isValid() { return this.amount.state >= 0; }
}

another would be to make it a no-op:

withdraw(amount) {
  let hypothetical = this.amount.decrement(-1*amount);
  if (this.balance.isValid) {
    return hypothetical;
  } else {
    return this;
}

Notice how because microstates are immutable we can invoke transitions to see what the result would be given the current arguments before deciding if that's what we actually want to do.

Still another way, and perhaps the most micro-state-y, state-machine-y way would be to use a "union" type to represent the different possible states.

class Balance {
  amount = Number;
  initialize(value) {
    let amount = Number
    if (amount < 0) {
      return create(NegativeBalance, value);
    else {
      return create(PositiveBalance, value);
     }
  }

  deposit(amount) {
    return this.amount.increment(amount);
  }
}

class PositiveBalance extends Balance {
  withdraw(amount) {
    return this.amount.decrement(amount);
  }
}

class NegativeBalance extends Balance {
   //there is no withdraw method at all.
}

let balance = create(Balance, 10);
balance.withdraw(5); //=> PositiveBalance
balance = balance.withdraw(10) //=> NegativeBalance
balance = balance.withdraw(20) //=> TypeError: balance.withdraw is not a function

This is cumbersome at the moment, but we have plans to add some functions to define these union types more succicntly. For now though, you have to do it manually. The pattern is:

  1. have an "abstract" super-class that discriminates between concrete subtypes based on the value in its initialize method.
  2. One concrete subtype for each state in your state machine. In this example, we have two states "positive" and "negative".
  3. Methods are transitions between states. If one of your states does not have that transition, then it won't have that method and you'll get a TypeError
  4. (optional) If every state has an arrow that transitions to the same state, you can move the method up to the superclass.

We're not entirely sure what the syntax will be, but something along the lines of:

import { Union } from 'microstates';

const Balance = Union({
  positive: class PositiveBalance {
    //positive definition goes here.
  },
  negative: class NegativeBalance {
    //negative definition goes here.
});

cowboyd avatar Oct 31 '18 19:10 cowboyd

Btw, I really like, @taras's solution here.

cowboyd avatar Nov 06 '18 19:11 cowboyd