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