Using Automat

Markus Wamser

2018-01-13

Usage of the Automat class is best demonstrated with a small example. We will implement the control logic of a (very simple) coffee vending machine using the Automat class, because coffee vending machines are usually programmed in R, right?

Creating the Coffee Vending Machine M

First of all, we want the package loaded and attached.

library(Wmisc)

A new machine is then easily produced.

M <- Automat$new()

Basic operation

Let’s add the simple operations: Assuming there are enough beans available, brew a nice cup of joe.

M$addTransition("beans available","button pressed","ground coffee",function(s,i,t){
  "grinding coffee"
})
M$addTransition("ground coffee","heat water","water heated",function(s,i,t){
  "heating water"
})
M$addTransition("water heated","brew coffee","coffee ready",function(s,i,t){
  "brewing coffee"
})
M$addTransition("coffee ready","take coffee","beans available",function(s,i,t){
  "enjoy your coffee"
})

M
## An Automat with 4 states.
## Current state is: not set

This is how out machine looks like:

M$visualize()

Before we can start an example execution, we need to set the initial state:

M$setState("beans available")
M
## An Automat with 4 states.
## Current state is: beans available
M$visualize()

And we can execute a basic lifecycle.

M$read("button pressed")
## [1] "grinding coffee"
M$read("heat water")
## [1] "heating water"
M$read("brew coffee")
## [1] "brewing coffee"
M$read("take coffee")
## [1] "enjoy your coffee"

But such a machine would get very bad usability results. I just want to press a single button to get my coffee. For this, the machine would have to go without further input or ‘spontaneus’ from one state to another. As an observer cannot determine when this will happen, this can be basically seen as a single transition and the states should be contracted. Alternatively, we can provide an external queue that holds the inputs and where the machine can push back inputs for itself.

Queue <- R6::R6Class("Queue",
                      public = list(
                        pop = function(){
                          r <- private$q[1]
                          private$q<-private$q[-1]
                          r
                        },
                        push = function(e){
                          private$q<-c(e,private$q)
                        },
                        append = function(e){
                          private$q<-c(private$q,e)
                        },
                        length = function(){
                          length(private$q)
                        }
                      ),
                      private = list(q=c())
                    )
q <- Queue$new()
M <- Automat$new()

M$addTransition("beans available","button pressed","ground coffee",function(s,i,t){
   q$push("heat water")
   "grinding coffee"
})
M$addTransition("ground coffee","heat water","water heated",function(s,i,t){
  q$push("brew coffee")
  "heating water"
})
M$addTransition("water heated","brew coffee","coffee ready",function(s,i,t){
  "brewing coffee"
})
M$addTransition("coffee ready","take coffee","beans available",function(s,i,t){
"enjoy your coffee"
})

M$setState("beans available")
q$append("button pressed")
q$append("take coffee")

while (q$length() > 0){
  print(M$read(q$pop()))
}
## [1] "grinding coffee"
## [1] "heating water"
## [1] "brewing coffee"
## [1] "enjoy your coffee"

As a last example, we look at a more complex variant:

M <- Automat$new()

M$addTransition("empty","fill beans","no water")
M$addTransition("empty","fill water","no beans")
M$addTransition("empty",NA,"empty",function(s,i,t){          # (1)
  print(paste0("'", i, "': that does not help, still ", t))
  return()
})

M$addTransition("no water","fill water","ready")
M$addTransition("no beans","fill beans","ready")

M$setPredicate("no beans",function(i){                       # (2)
  if (grepl("fill beans|coffee",i)){
    return("fill beans")
  }
  return(i)
})

M$addTransition("ready","press button","coffee ready",function(s,i,t){
  print("grinding coffee")                                   # (3)
  print("heating water")
  print("brewing coffee")
  print("please take your coffee")
  return()
})

M$addTransition("coffee ready","take coffee","ready",function(s,i,t){
  print("enjoy your coffee")
  return()
})

M$addTransition(NA,"clean machine","empty")                  # (4)
M$addTransition(NA,NA,"awaiting service",function(s,i,t){    # (5)
  print(paste0("Thou shall not ",i," in state (",s,").\nPlease clean the machine."))
  return()
})         

M$addTransition(NA,NA,NA,function(s,i,t){                    # (6)
  print(paste0("Log: (",s,",",i,",",t,")"))
})         

M$setState("empty")

This example shows the flexibility of the Automat class. At (1) we have a “by-any”-transition. If none of the explictly state inputs is matched, this wildcard transition matches and is taken. The complementary “from-any” transition type is used at (4): Whenever no explicit transition nor a “by-any”-transition match, the “from-any”-transitions are scanned for matching inputs. In the example a “clean machine” is accepted in any state and leads to an “empty” machine. Finally a default or fallback action is set at (5). Whenever something unexpected happens to the coffee machine it blocks and awaits to be cleaned. This is not to be confused with the always-action set at (6). It is not a true transition and provides a hook for a function that is always executed after a valid transition. Here it is used to provide logging. Another feature is shown at (2): Each state can be outfitted with a predicate function, that pre-processes the input before it is matched againt the possible transitions. Here you can say “fill beans” or “fill coffee” to get from “no beans”to “ready”.

M$visualize()

The Machine.

Above is the transition graph of the coffee machine. Transitions that invoke a function are marked with “f()”.

M$read("fill tea")
## [1] "'fill tea': that does not help, still empty"
## [1] "Log: (empty,fill tea,empty)"
M$read("fill water")
## [1] "Log: (empty,fill water,no beans)"
M$read("fill coffee")
## [1] "Log: (no beans,fill coffee,ready)"
M$read("press button")
## [1] "grinding coffee"
## [1] "heating water"
## [1] "brewing coffee"
## [1] "please take your coffee"
## [1] "Log: (ready,press button,coffee ready)"
M$read("take coffee")
## [1] "enjoy your coffee"
## [1] "Log: (coffee ready,take coffee,ready)"
M$read("press button")
## [1] "grinding coffee"
## [1] "heating water"
## [1] "brewing coffee"
## [1] "please take your coffee"
## [1] "Log: (ready,press button,coffee ready)"
M$read("press button")
## [1] "Thou shall not press button in state (coffee ready).\nPlease clean the machine."
## [1] "Log: (coffee ready,press button,awaiting service)"
M$read("fill coffee")
## [1] "Thou shall not fill coffee in state (awaiting service).\nPlease clean the machine."
## [1] "Log: (awaiting service,fill coffee,awaiting service)"
M$read("clean machine")
## [1] "Log: (awaiting service,clean machine,empty)"
M$read("fill beans")
## [1] "Log: (empty,fill beans,no water)"
M$read("fill water")
## [1] "Log: (no water,fill water,ready)"