Using states that are more than a single atom

AshStateMachine is all about transitions between states, and the state is represented by an atom. But transitions are only one part of FSMs, in general we want them to react on inputs, and the state is more than a single constant value.

Let’s consider a simple coffee vending machine that can make several types of drinks, each having its own price. In its initial state, the machine will only accept one input - “coin inserted”, and that input will transition to “drink selection” state. The input has an additional property – the coin’s monetary value, and the “drink selection” state should also store that monetary value. In “drink selection” state, two inputs can be accepted: “coin inserted”, which will keep the machine in “drink selection” state and increase the stored monetary value, and “drink selected” input which will transition to “drink preparation” state, but only if enough coins were inserted to cover the cost of the selected drink.

Reading the documentation, I do not see a declarative way to define that state machine. Am I missing something?

The main thing that makes ash_state_machine different in design from other state machine tools is that the actions are still the source of truth. The state transitions defined in the state machine block describe what transitions are allowed, and then in a given action you actually perform the state transition.

attributes do
  attribute :unspent_cents, :integer, allow_nil?: false, default: 0 
end

state_machine do
  transitions do
    initial_states [:waiting_for_coin]

    transition :coin_inserted, from: :waiting_for_coin, to: :drink_selection
    transition :drink_selected, from: :drink_selected, to: :drink_preparation
  end
end

actions do
  update :coin_inserted do
    argument :coin_amount, :integer, allow_nil?: false
    change atomic_update(:unspent_cents, expr(unsepent_cents + ^arg(:coin_amount))
    change transition_state(:drink_selection)
  end

  update :drink_selected do
    argument :selection, :atom, constraints: [one_of: [:coke, :diet_coke]]
    change set_attribute(:drink_selection, arg(:selection))
    change transition_state(:drink_preparation), where: [CanAffordDrinkSelection]
  end
end

This is just an example, certainly not 100% correct, but thrown together as an example of what it looks like in practice.

1 Like

Thanks! That makes things much more clear