Having recently read a post about using state machines in JS, I decided to try implementing the same logic using the (relatively) new gen_statem.
As ever, the code is available here.
The initial example is a naive representation of a bank account:
The first step is to fill out the gen_statem boilerplate:
-module(bank_statem). -behaviour(gen_statem). -export([init/1, callback_mode/0]). -export([open/3]). init([]) -> {ok, open, #{balance=>0}}. callback_mode() -> state_functions. open({call, From}, get_balance, #{balance:=Balance} = Data) -> {keep_state, Data, [{reply, From, Balance}]}.
We return an initial state of open
, and a map containing the initial balance of 0.
I also added a get_balance
call, so we can inspect the current data (which would normally be referred to as the “state” in a gen_server, confusingly).
The first state transition is to close an open account:
open({call, From}, close, Data) -> {next_state, closed, Data, [{reply, From, closed}]};
The function name tells you which state this can be called from (open), and the return value tells you that the state machine would transition to a new state (closed), as well as returning a value (the atom closed
) to the caller.
Re-opening an account is pretty similar:
closed({call, From}, reopen, Data) -> {next_state, open, Data, [{reply, From, open}]}.
As close is only defined for the open state, and reopen is only defined for the closed state, any inappropriate calls will cause the process to crash.
Deposit & withdraw are a little different:
open({call, From}, {deposit, Amount}, #{balance:=Balance} = Data) when is_number(Amount) andalso Amount > 0 -> NewBalance = Balance + Amount, {keep_state, Data#{balance:=NewBalance}, [{reply, From, deposit_made}]}; open({call, From}, {withdraw, Amount}, #{balance:=Balance} = Data) when is_number(Amount) andalso (Balance - Amount > 0) -> NewBalance = Balance - Amount, {keep_state, Data#{balance:=NewBalance}, [{reply, From, withdrawal_made}]}.
In that they retain the current state, but the data (in this case, the balance) is updated. Guards have also been used to validate the arguments; again, any other calls will cause an error.
This will seem pretty familiar, if you’ve ever used gen_fsm. Next time, we’ll look at the new alternative callback mode: handle_event_function
.