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

Guards or how to fail a transition #260

Open
jeffutter opened this issue Oct 31, 2018 · 3 comments
Open

Guards or how to fail a transition #260

jeffutter opened this issue Oct 31, 2018 · 3 comments

Comments

@jeffutter
Copy link
Contributor

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?

@taras
Copy link
Member

taras commented Oct 31, 2018

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?

@cowboyd
Copy link
Member

cowboyd commented Oct 31, 2018

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
Copy link
Member

cowboyd commented Nov 6, 2018

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants