1   Introduction

In an earlier post, I discussed the implementation of an FSM (finite state machine) in Python using the transitions package (see: Defining and implementing finite state machines in Python with the transitions module). In this (current) post I discuss the implementation of FSM in Erlang with the gen_statem behavior.

Erlang OTP has two modules that help with implementing FSMs: gen_fsm (see earlier post) and the newer module gen_statem. In this post, I'll discuss the use of gen_statem.

gen_statem actually provides two separate modes for implementing the functionality in an FSM:

  • You can implement a single event handler function. This function is passed the current state, and must dispatch on that. Then the code that handles each initial state must check the conditions that must be true before a transition is taken.
  • You can implement one function for each state. The name of the function is the state name. The function for a given state must check the condition that must be true before a transition is taken.

You need to tell the gen_statem behavior which of the two above styles you intend to implement. To do so, implement a callback function:

callback_mode() ->
    %handle_event_function.
    state_functions.

2   The task for our FSM

This FSM does roughly the same task as the FSM we implemented in Python with transitions module in an earlier post (see: Defining and implementing finite state machines in Python with the transitions module). It takes a string of characters as input, and converts all characters inside double quotes to upper case.

3   Converting rules to FSM functions

One way to "picture" an FSM is a digraph (a directed graph) in which the nodes are states and the edges (arrows) are the transitions. And, you can label the edges with (1) the condition that is required to be True in order for that transition to be selected and (2) the action to be taken if that transition is selected.

But, an alternative way to describe an FSM is a table of rules. There is one rule for each possible transition, and the table has one row for each rule. The table typically has four columns: (1) initial state; (2) condition that must be true for the rule to be selected; (3) action to be performed when that rule is selected; and (4) destination or next state.

A rule table is often helpful, in part, because you can check off the rules, one by one, as you translate each one into code. You can even annotate the rules with a rule number and include a comment in the code indicated where the each rule is implemented.

The rules table:

Rule no. State 1 Condition Action State 2
1 start Quote char [None] in_quotes
2 start Not quote char [None] not_in_quotes
3 start 'end' char [None] finish
4 in_quotes Quote char [None] not_in_quotes
5 in_quotes Non-quote char Action 1 in_quotes
6 in_quotes 'end' char Action 2 finish
7 not_in_quotes Quote char [None] in_quotes
8 not_in_quotes Non-quote char Action 3 not_in_quotes
9 not_in_quotes 'end' char Action 4 finish
10 finish [None] Action 5 start

Actions:

  • Action 1 -- Convert character to upper case. Save character in accumulator.
  • Action 2 -- Return error code.
  • Action 3 -- Save character in accumulator.
  • Action 4 -- Return the accumulated string of converted characters.
  • Action 5 -- Reset the FSM. Set next state to 'start'.

Writing the above table is a bit tedious. But, once you have the table, translating it into Erlang code is reasonably straight-forword. Here is some guidance to help you with implementing the above table with the Erlang and gen_statem behavior:

  1. Create a function for each state. The name of the function is the same as the name of the state. (If the state name is an Erlang keyword, put single quotes around it. Here is the signature for the function:

    StateName({call, From}, EventContent, StateData)
    

    Here we are assuming that events are triggered with the gen_statem:call. If you trigger an event by using gen_statem:cast, then the handler signature should be:

    StateName(cast, EventContent, StateData)
    
  2. Inside that function, write a case or if statement to check for the condition from the Rules Table. Include one clause for each condition to be checked for that state (function). Notice how I've arranged or sorted our example rule table above so that all rules with the same initial state are together. If there are three rules with initial state state_one, then you will likely have three clauses in your case or if statement in function state_one..

  3. Return the next state from the rules table. For example, either of the following will work:

    {next_state, NextStateName, State, [{reply, From, ReturnValue}]}
    

    or:

    {next_state, NextStateName, State}
    

    If you need to return a value to the caller, use the first; if not, use the second.

  4. If you need to pass data back to the caller (i.e. return a value), then you can provide it as an action in the function's return value. For example:

    {next_state, NextStateName, NewStateData, [{reply, From, "data for caller"}]}
    

4   The implementation and the code

Here is the actual code: characters01.erl

Notes:

  • This module was initially generated by using an Erlang template for the Vim text editor. See: https://github.com/vim-erlang/vim-erlang-skeletons.

  • Our state and event handling functions are start, in_quotes, not_in_quotes, and finish. Note that there are actually two functions named start in this module, but that does not cause a conflict because they have a different arity/signature.

  • We also provide an API for the client that uses our FSM. The function start and stop enable the client to start and stop the server; the function feed enables the client to feed a sequence of characters to the FSM so that they can be converted.

  • The internal function feed_chars at the bottom of the module feeds characters, one character at a time, to the FSM by calling:

    gen_statem:call(?SERVER, {char, Char}),
    

- Dave Kuhlman