State Machines
There are various ways to implement state machines with actors. The following examples illustrate how actors operate as state machines and are not to propose actual implementations.
DFAs, behavior-switch
Behavior-switch is the most elegant way to implement a finite state machine with an actor.
Take a deterministic finite automaton with four states $\{s,q,p,r\}$, three inputs $\{a,b,c\}$ and the transition function $δ$ represented in a table:
| $\;$ | $\;$ | a | b | c |
|---|---|---|---|---|
| initial state | -> s | s | q | s |
| p | p | q | p | |
| q | p | r | r | |
| accepting state | * r | r | r | r |
The state-transition table can be implemented directly:
using YAActL
s(::Val{'b'}) = become(q) # implement behaviors
s(x) = nothing # default transition
p(::Val{'b'}) = become(q)
p(x) = nothing
q(::Val{'a'}) = become(p)
q(::Val{'b'}) = become(r)
q(::Val{'c'}) = become(r)
q(x) = nothing
r(x) = nothingDFA-states are directly represented by behavior functions. Those switch behavior/state on certain input values or otherwise do nothing.
The actor ist started with the initial behavior. A check function parses input strings:
mydfa = Actor(s)
function check(lk::Link, str::String)
become!(lk, s) # switch to initial state
foreach(str) do c
cast!(lk, Val(c)) # send all chars to the actor
end
query!(lk, :bhv) == r # check the final state
endIt checks which strings get accepted:
julia> check(mydfa, "baab")
false
julia> check(mydfa, "baabc")
true
julia> check(mydfa, "babaccabb")
trueNFAs, state-dispatch
Non-deterministic finite automata can have more complex states, and more than one transition can happen at an input event.
| $\;$ | $\;$ | a | b | c |
|---|---|---|---|---|
| initial state | -> s | {t,v,w} | ∅ | ∅ |
| t | {u,v} | {u,v} | ∅ | |
| accepting state | * u | {s,w} | {s,w} | ∅ |
| accepting state | * v | ∅ | {t,v} | {t,v} |
| w | ∅ | {t,v} | ∅ |
Now a simple state cannot represent directly this NFA. A transition function $\delta$ is used to return and to dispatch on a Tuple of states:
using YAActL, .Iterators
@enum Q s t u v w # describe states
# implement the transition function/methods
δ(::Val{s}, ::Val{'a'}) = (t,v,w)
δ(::Val{s}, ::Any) = (s,) # default transition
δ(::Val{t}, ::Val{'a'}) = (u,v)
δ(::Val{t}, ::Val{'b'}) = (u,v)
δ(::Val{t}, ::Any) = (t,)
δ(::Val{u}, ::Val{'a'}) = (s,w)
δ(::Val{u}, ::Val{'b'}) = (s,w)
δ(::Val{u}, ::Any) = (u,)
δ(::Val{v}, ::Val{'b'}) = (t,v)
δ(::Val{v}, ::Val{'c'}) = (t,v)
δ(::Val{v}, ::Any) = (v,)
δ(::Val{w}, ::Val{'b'}) = (t,v)
δ(::Val{w}, ::Any) = (w,)
δ(q::Q, c::Char) = δ(Val(q), Val(c)) # function barrier
δ(qs::Tuple, c::Char) = [δ(q,c) for q in qs] |> flatten |> Set |> TupleTo illustrate how an actor works in state dispatch mode, we implement a simple iteration function to parse an input string and an actor method:
# do a simple iteration with a local state variable
function check(str::String)
qs = s # set the initial state 's'
for c in str
qs = δ(qs, c) # update state
end
intersect(qs, (u,v)) |> !isempty
end
# work with the actor in state dispatch
function check(lk::Link, str::String)
update!(mynfa, s) # set initial state
foreach(c->cast!(lk,c), str) # cast each character
intersect(query!(lk), (u,v)) |> !isempty # check state
endBoth implementations follow exactly the same logic. But in the second case the actor maintains state. This enables asynchronous operation: we can send single characters and query the state anytime.
An actor is setup in state dispatch mode and checks strings:
mynfa = Actor(δ) # an actor with a δ behavior
set!(mynfa, state) # is set to state dispatch
...
julia> check(mynfa, "aabc")
true
julia> check(mynfa, "bbb")
falseFibonacci Server
Servers often store data and provide it to clients with some additional computation. One classic example of that is a Fibonacci server. It stores calculated fibonacci numbers in a Dict in order to be able to serve future calls faster:
using YAActL
function fib(D::Dict{Int,BigInt}, n::Int)
get!(D, n) do
n == 0 && return big(0)
n == 1 && return big(1)
return fib(D, n-1) + fib(D, n-2)
end
endSince D is a mutable variable and fib returns the result, we use the actor's full dispatch mode. We start it with a new empty Dict and can call! it with the desired number n. The actor then updates D as necessary:
myfib = Actor(fib, Dict{Int,BigInt}())
julia> call!(myfib, 1000)
43466557686937456435688527675040625802564660517371780402481729089536555417949051890403879840079255169295922593080322634775209689623239873322471161642996440906533187938298969649928516003704476137795166849228875The dictionary D is private to the actor.
Generic Function Servers
YAActL actors support directives like:
become!: cause an actor to switch its behavior,call!an actor to execute its behavior function and to send the result,cast!: cause an actor to execute its behavior function,exec!: tell an actor to execute a function and to send the result.
They operate as state machines for executing functions. This makes them generic function servers. As shown above the behavior functions may implement state machines as well. They can start other child or siblings actors for doing things. Thus they can represent a hierarchy of state machines.