A simple finite state machine in Erlang and F#

Yan Cui

I help clients go faster for less using serverless technologies.

Considering a very simple finite state machine (FSM) such as a code lock which requires you to enter a 4-digit numeric code and upon which will open the lock for a brief few seconds.

imageSuch a FSM have two states: Locked and Open.

Every time a key is entered it checks the sequence of digits inputted so far and if the sequence does not match the secret code then it stays in the Locked state.

If the input sequence matches the length of the secret code then the user is judged to have entered an incorrect sequence and the input sequence is reset.

If, on the other hand, the user has entered the correct code then the FSM will change to the Open state for a number of seconds before reverting back to the Locked state.

This post is intended to show you how you can implement this FSM easily using F#’s agent and Erlang OTP’s gen_fsm behaviour.

 

 

 

F#

First, let’s define the different states that our FSM can be in:

image

and then the messages that our agent can receive:

image

The actual FSM implementation looks like this:

namespace Fsm
open System
open System.Threading
open FSharp.Control
type State = | Locked of int list
| Open
| Closed
type Message = | Button of int
| Close of AsyncReplyChannel<unit>
| GetState of AsyncReplyChannel<State>
type CodeLock (code : int list) =
let code = code |> List.rev
let doUnlock () = printfn "The code lock is now unlocked"
let doLock () = printfn "The code lock is now locked"
let doCleanUp () = printfn "The code lock is now terminated..."
let cts = new CancellationTokenSource()
let agent = Agent<Message>.Start((fun inbox ->
let rec lockedState (soFar : int list) =
async {
let! msg = inbox.Receive()
match msg with
| Button(digit) when soFar.Length < code.Length - 1
-> return! lockedState (digit::soFar)
| Button(digit) when (digit::soFar) = code
-> doUnlock()
return! openState ()
| Button(digit) -> return! lockedState []
| GetState(reply) -> reply.Reply(Locked soFar)
return! lockedState soFar
| Close(reply) -> doCleanUp()
reply.Reply()
cts.Cancel()
| _ -> return! lockedState soFar
}
and openState () =
async {
try
let! msg = inbox.Receive(3000)
match msg with
| GetState(reply)
-> reply.Reply(Open)
return! openState()
| Close(reply)
-> doCleanUp()
reply.Reply()
cts.Cancel()
| _ -> return! openState()
with
| :? TimeoutException -> doLock()
return! lockedState []
}
lockedState []), cancellationToken = cts.Token)
member this.Button digit = agent.Post <| Button(digit)
member this.GetState () =
try
agent.PostAndReply(GetState, 1000)
with
| :? TimeoutException -> Closed
member this.Close () = agent.PostAndReply Close

This implementation is pretty straight forward, I’ve used a similar technique to the one Tomas Petricek used in his BlockingQueueAgent implementation and represented the different states using recursive functions.

One caveat with this FSM is the need to revert back to the Locked state after being in the Open state for a certain amount of time. For simplicity sake, I’ve simply specified a timeout (3 seconds) when waiting for messages in the Open state and use the TimeoutException as cue to go back to the Locked state. The obvious downside to my implementation here is that if a GetState message is received it will reset the timer!

You can fix this easily enough in a number of ways, including using a mutable instance variable to keep track of the current state and let the async lambdas modify it during state changes and transitions, then the GetState() method would no longer need to trouble the agent to find out what the current state is.

 

Erlang

Erlang’s OTP framework provide a gen_fsm behaviour which you can use to implement a FSM, which will take care of most of the plumbing and orchestration for you leaving you free to focus on the important things – handling the different ‘events’ that can be triggered during different states.

%% Description: A simple FSM for a code lock with three states
%% based on an example from the OTP system documentation
-module(code_lock_fsm).
-behaviour(gen_fsm).
%% public API functions
-export([start_link/1, stop/0, button/1, reset/0, get_state/0]).
%% state names
-export([locked/2, locked/3, open/2, open/3]).
%% gen_fsm callback functions
-export([init/1, handle_event/3, handle_sync_event/4, terminate/3]).
%%% API functions
%% gen_fsm:start_link will spawn and register new process and register it
%% so that it's locally known as code_lock
%% it'll then call the init/1 function in the callback module (2nd param)
%% with the argument (3rd param)
start_link(Code) ->
gen_fsm:start_link({local, code_lock}, ?MODULE, Code, []).
%% this callback function is called by gen_fsm:start_link above if
%% registration was successful
%% this function is expected to return { ok, StateName, StateData }
%% where StateName is the initial state - locked with initial state
%% data being the tuple { [], lists:reverse(Code) }
%% note: the Code needs to be reversed so that [1, 2, 3, 4] can be entered
%% as 1, 2, 3, 4 as opposed to 4, 3, 2, 1 because of the way the inputs
%% list is built up in the locked state
init(Code) ->
{ ok, locked, { [], lists:reverse(Code) } }.
get_state() ->
try
gen_fsm:sync_send_event(code_lock, get_state)
catch
exit : { noproc, _ } -> closed
end.
%% sends a 'button' event to the FSM registered as code_lock, when the event
%% is received by the FSM, gen_fsm calls StateName(Event, StateData)
%% which is expected to return { next_state, NextStateName, NextStateData }
%% where NextStateName is the next state to move into and NextStateData is
%% the new state data for gen_fsm
button(Digit) ->
gen_fsm:send_event(code_lock, { button, Digit }).
%% this callback function is called when the FSM is in the 'locked' state
%% and the 'button' event is fired
locked({ button, Digit }, { SoFar, Code }) ->
case [Digit|SoFar] of
Code ->
% if the new digit completes a sequence that matches the code then
% unlocks the lock
do_unlock(),
% after the time out (3 seconds) has expired, the callback function
% StateName(timeout, StateData) is called, which in this calls
% will be open(timeout, { [], Code })
{ next_state, open, { [], Code }, 3000 };
Incomplete when length(Incomplete) < length(Code) ->
% if the new digit does not add to a sequence that matches the
% length of the code then stay in the 'locked' state but update
% the state data to include the new digit
{ next_state, locked, { Incomplete, Code } };
_Wrong ->
% the sequence now matches the code but it's wrong, stay in 'locked'
% state and clear the digits entered so far
{ next_state, locked, { [], Code } }
end;
locked(Event, State) ->
io:format("Unexpected event received in locked state : ~p", [Event]),
{ next_state, locked, State }.
locked(get_state, _From, State = { SoFar, _ }) ->
{ reply, { locked, SoFar }, locked, State }.
open(timeout, State) ->
% after timeout expires in the 'open' state, go into 'locked' state again
do_lock(),
{ next_state, locked, State }.
open(get_state, _From, State) ->
{ reply, open, open, State, 3000 }.
%% gen_fsm:sync_send_all_state_event fires an event to the FSM regardless of
%% its state and can be handled with a handle_event/3 function defined in the
%% callback module
stop() ->
gen_fsm:sync_send_all_state_event(code_lock, stop).
reset() ->
gen_fsm:send_all_state_event(code_lock, reset).
handle_event(stop, _StateName, State) ->
{ stop, normal, State };
handle_event(reset, _StateName, { _, Code }) ->
{ next_state, locked, { [], Code } }.
handle_sync_event(stop, _From, _StateName, State) ->
{ stop, normal, State };
handle_sync_event(reset, _From, _StateName, { _, Code }) ->
{ next_state, locked, { [], Code } };
handle_sync_event(get_state, _From, StateName, State) ->
{ reply, { StateName, State }, StateName, State }.
terminate(normal, _StateName, _State) ->
do_clean_up(),
ok;
terminate(shutdown, _StateName, _State) ->
do_clean_up(),
ok.
%%% Private functions
do_unlock() ->
io:format("The code lock is now unlocked~n").
do_lock() ->
io:format("The code lock is now locked~n").
do_clean_up() ->
io:format("The code lock is now termianted...~n").

This Erlang implementation might look much bigger than its F# counterpart but keep in mind that I’ve included plenty of comments here and not been as conservative with write spaces as I was with the F# code. All and all, both versions require roughly the same number of LOC, the main difference being the lack of plumbing in the Erlang version, which if I have to be honest, can be confusing if you don’t have a decent understanding of how the gen_fsm behaviour works!

You can download and play around with the full source code (with corresponding tests for each implementation) from Github here. Enjoy!

Whenever you’re ready, here are 3 ways I can help you:

  1. Production-Ready Serverless: Join 20+ AWS Heroes & Community Builders and 1000+ other students in levelling up your serverless game. This is your one-stop shop for quickly levelling up your serverless skills.
  2. I help clients launch product ideas, improve their development processes and upskill their teams. If you’d like to work together, then let’s get in touch.
  3. Join my community on Discord, ask questions, and join the discussion on all things AWS and Serverless.

1 thought on “A simple finite state machine in Erlang and F#”

  1. Nice article and a great way of presenting the implementation difference between two extremely potential languages.
    In the erlang version, it would have been better if a small change is made in the initialization values specified in gen_fsm:start_link({local, code_lock}, ?MODULE, Code, []).

    I believe the Code should be entered in the form of a list like gen_fsm:start_link({local, code_lock}, ?MODULE, [Code], []), which is the industry practice.

    Never mind, this version should still work if its entered like ?MODULE:start_link([1234[). in the erlang prompt. User himself has to enter the input as list.

Leave a Comment

Your email address will not be published. Required fields are marked *