This is a list of my take­aways from the excel­lent talk between Erik Mei­jer (of the LINQ and Rx fame), Carl Hewitt (cre­ator of the Actor model) and Clemens Szyper­ski, on the Actor model.

 

Dis­claimer : this con­ver­sa­tion revolves around the con­cep­tual model of an actor, as opposed to spe­cific imple­men­ta­tions of the Actor model.

 

What is an actor?

An actor is the fun­da­men­tal unit of com­pu­ta­tion which embod­ies the 3 things – pro­cess­ing, stor­age and com­mu­ni­ca­tions – that are essen­tial to computation.

One actor is no actor, they come in sys­tems, and they have to have addresses so that one actor can send mes­sages to another actor.

 

Beyond the high-level abstrac­tion, an actor has a num­ber of properties:

  • Every­thing is an actor
  • An actor has a mailbox

 

Since a mail­box is also an actor, it too will have a mail­box, and so the recur­sion begins! This recur­sion ends with axioms.

 

Axiom

When an actor receives a mes­sage it can:

  • Cre­ate new actors
  • Send mes­sages to actors it has addresses before
  • Des­ig­nate how to han­dle the next mes­sage it receives (e.g. state)

and that’s it!

“Con­cep­tu­ally, mes­sages are processed one at a time, but the imple­men­ta­tion can allow for con­cur­rent pro­cess­ing of mes­sages.” – Carl Hewitt

 

This is not the same as a con­tin­u­a­tion, which is the lambda expres­sion that you exe­cute after doing the cur­rent one and is a con­cept for sin­gle threaded processing.

 

Whilst con­cep­tu­ally mes­sages are processed one at a time, the imple­men­ta­tion can allow for con­cur­rent pro­cess­ing of mes­sages. For instance, a fac­to­r­ial actor which has no state and will process each mes­sage the same way can process an arbi­trary num­ber of mes­sages at the same time.

 

An actor can also send mes­sages to itself (i.e. recur­sion), and to avoid dead­locks we have the notion of a future.

The idea of a future is that you can cre­ate an actor with any result whilst it’s still being com­puted. For instance, you can cre­ate a future for fac­to­r­ial 100m, which will take a long time to com­pute, but you can have the future straight away and pass it around.

 

Addresses

The address of an actor is not the same as its iden­tity because:

  • One actor can have one address for many actors if you’re repli­cat­ing behind the scenes
  • One actor can have many addresses that for­ward to one another (via proxy actors)

hence there’s a many-to-many rela­tion­ship between actors and addresses.

 

With actors, all you have are addresses, which doesn’t tell you whether you have one or many actors behind those addresses. The same notion of addresses also applies to the web, e.g. whilst search­ing on google.com it’s not the same actor that are pro­cess­ing your requests every time.

 

Addresses are sim­i­lar to capa­bil­i­ties, but is a much clearer name for a capa­bil­ity because it tells you exactly what you are allowed to do – send­ing mes­sages to it, which is its only capability.

“If you can main­tain the integrity of addresses, you get capa­bil­i­ties for free” – Carl Hewitt

 

Mes­sages

Mes­sages are like ‘pack­ets’ in the inter­net, they obey the same rule as pack­ets for effi­ciency rea­sons – mes­sages are received in any order because it’s more expen­sive on the sys­tem to enforce the order­ing constraint.

 

Mes­sages are also deliv­ered on a best-efforts basis, which when cross­ing machines this means they are per­sisted on some stor­age and can be resent if receipt acknowl­edge­ment is not received. But if the source machine is ter­mi­nated before the resent hap­pens then the mes­sage is lost.

 

Mes­sages sent between actors are deliv­ered at most once, and may take a long time to arrive depend­ing on dis­tance and net­work latency between the actors (e.g. mes­sage in a bottle..).

 

Chan­nels

“There are no chan­nels” – Carl Hewitt

Instead, the actors talk directly to one another.

 

The prob­lem with a chan­nel is that if you’re try­ing to send a mes­sage to two recip­i­ents only one of them will receive the mes­sage, unless you go through with the over­head of a two-phase com­mit.

 

As an imple­men­ta­tion detail, you can imple­ment a chan­nel (which will be another actor in the sys­tem) if you want, but it’s not part of the con­cep­tual model.

 

Non­de­ter­min­ism vs Indeterminism

A quick recap on tur­ing machines, which is the­o­ret­i­cal machine that defines com­putabil­ity. It can be thought of as a sim­ple com­puter that reads and writes sym­bols one at a time on an infi­nitely long tape by fol­low­ing a set of rules. It deter­mines what to do next accord­ing to an inter­nal state and what sym­bol it cur­rently sees on the tape.

In a deter­min­is­tic tur­ing machine, given the cur­rent state and sym­bol it spec­i­fies only one action to be per­formed. For exam­ple, “if you are in state 2 and you see an ‘A’, write a ‘B’ and move left”.

In a non­de­ter­min­is­tic tur­ing machine (NTM), given the cur­rent state and sym­bol it may spec­ify more than one action to be per­formed. For exam­ple, “if you are in state 2 and you see an ‘A’, write a ‘B’, move right and switch to state 5”.

 

In a NTM, the state of the com­pu­ta­tion is fixed, and can be proved that a state machine model of com­pu­ta­tion has to have a bounded non­de­ter­min­ism (i.e. it halts after a bounded num­ber of steps, hence has a bounded num­ber of pos­si­ble configurations).

With the Actor model, you have a configuration-based model of com­pu­ta­tion (based on mes­sages that are received, which are dynamic as opposed to fixed), which is more pow­er­ful because it incor­po­rates com­mu­ni­ca­tion. This configuration-based model gives you inde­ter­min­ism, which is what hap­pens when things work them­selves out.

 

Con­trary to pop­u­lar believes, tur­ing machine is not the only thing that defines com­putabil­ity, and inter­ac­tions with an open envi­ron­ment cer­tainly changes what com­pu­ta­tion means and is the dif­fer­ence between non­de­ter­min­ism and indeterminism.

 

Syn­chro­niza­tion

Syn­chro­niza­tion is built into the Actor model because mes­sages can be received one at a time by an actor.

In a check-in account exam­ple where many par­ties can cash-in or with­draw from the account, sup­pose the cur­rent bal­ance is £2, and one per­son tries to with­draw £7 whilst another tries to cash-in £8, the out­come is inde­ter­mi­nant based on the order in which the mes­sages are received by the actor.

This is where the arbiters come in.

 

Arbiter

“The arbiter decides, and there’s noth­ing before the arbiter decides” – Carl Hewitt

Given an arbiter, you can have mul­ti­ple inputs (e.g. I0 and I1) into the arbiter at the same time, but only one of the pos­si­ble out­comes (e.g. O0 or O1) will come out on the other end.

image

The arbiter is what gives us inde­ter­min­ism, it can take an arbi­trary amount of time (with the prob­a­bil­ity of inde­ci­sion decreas­ing expo­nen­tially over time) to come to a deci­sion but it must decide.

 

Imple­men­ta­tion

There’s an art to the imple­men­ta­tion of the Actor model in pro­gram­ming lan­guages and there are many ways you can make mis­takes in the imple­men­ta­tion – by vio­lat­ing some of the fun­da­men­tal prin­ci­ples or by not tak­ing them seriously.

 

The Actor model is not the same as tail recur­sive calls (because it can change the state for the next mes­sage received) or event loops (because of the optimizations).

 

 

I hope I’ve done the talk jus­tice with these short notes I’ve taken and that you find them use­ful as you no doubt watch the talk over and over as I had, and before we go I’d like to leave you with yet another great quoteSmile

“We don’t know much, and some of it is wrong” – Carl Hewitt

Share

You can spec­ify a func­tion which can take in a numeric value with a generic unit of mea­sure eas­ily enough:

image

Sim­i­larly, you can also spec­ify a dis­crim­i­nated union whose clauses can be of a numeric value with a generic unit of mea­sure, like this:

Share

Pecu­liarly I couldn’t find any doc­u­mented way to cre­ate a type exten­sion for a generic array, ‘a [ ], turns out you need to use back­tick marks ( ‘ ) around the square brack­ets in order to do that:

Share

Con­sid­er­ing a very sim­ple 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 dig­its 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 incor­rect sequence and the input sequence is reset.

If, on the other hand, the user has entered the cor­rect code then the FSM will change to the Open state for a num­ber of sec­onds before revert­ing back to the Locked state.

This post is intended to show you how you can imple­ment this FSM eas­ily using F#’s agent and Erlang OTP’s gen_fsm behaviour.

 

 

 

F#

First, let’s define the dif­fer­ent states that our FSM can be in:

image

and then the mes­sages that our agent can receive:

image

The actual FSM imple­men­ta­tion looks like this:

This imple­men­ta­tion is pretty straight for­ward, I’ve used a sim­i­lar tech­nique to the one Tomas Pet­ricek used in his Block­ingQueueAgent imple­men­ta­tion and rep­re­sented the dif­fer­ent states using recur­sive functions.

One caveat with this FSM is the need to revert back to the Locked state after being in the Open state for a cer­tain amount of time. For sim­plic­ity sake, I’ve sim­ply spec­i­fied a time­out (3 sec­onds) when wait­ing for mes­sages in the Open state and use the Time­ou­tEx­cep­tion as cue to go back to the Locked state. The obvi­ous down­side to my imple­men­ta­tion here is that if a Get­State mes­sage is received it will reset the timer!

You can fix this eas­ily enough in a num­ber of ways, includ­ing using a muta­ble instance vari­able to keep track of the cur­rent state and let the async lamb­das mod­ify it dur­ing state changes and tran­si­tions, then the Get­State() method would no longer need to trou­ble the agent to find out what the cur­rent state is.

 

Erlang

Erlang’s OTP frame­work pro­vide a gen_fsm behav­iour which you can use to imple­ment a FSM, which will take care of most of the plumb­ing and orches­tra­tion for you leav­ing you free to focus on the impor­tant things – han­dling the dif­fer­ent ‘events’ that can be trig­gered dur­ing dif­fer­ent states.

This Erlang imple­men­ta­tion might look much big­ger than its F# coun­ter­part but keep in mind that I’ve included plenty of com­ments here and not been as con­ser­v­a­tive with write spaces as I was with the F# code. All and all, both ver­sions require roughly the same num­ber of LOC, the main dif­fer­ence being the lack of plumb­ing in the Erlang ver­sion, which if I have to be hon­est, can be con­fus­ing if you don’t have a decent under­stand­ing of how the gen_fsm behav­iour works!

You can down­load and play around with the full source code (with cor­re­spond­ing tests for each imple­men­ta­tion) from Github here. Enjoy!

Share

Related Posts

 

Here I will look at how some com­monly used func­tions in F#’s List mod­ule might be trans­lated to Erlang using Erlang’s equiv­a­lent – the lists module.

 

List.append

F#:         let newList = List.append [ 1..5 ] [ 6..10 ]

Erlang:   NewList = lists:append([ 1, 2, 3, 4, 5 ], [ 6, 7, 8, 9, 10 ]).

List.average

F#:         let avg = List.average [ 1.0..10.0 ]

Erlang:   List = lists: seq(1, 10).

             Avg = lists: sum(List) / length(List).

List.collect

F#:         let duped = [ ‘a’; ‘b’; ‘c’ ] |> List.collect (fun x -> [ x; x ])

Erlang:   Duped = lists:flatmap(fun(X) –> [ X, X ] end, [ a, b, c ]).

List.concat

F#:         let newList = List.concat [ [ 1; 2 ]; [ 3; 4; 5 ]; [ 6; 7; 8; 9 ] ]

Erlang:   NewList = lists:append([ [ 1, 2 ], [ 3, 4, 5 ], [ 6, 7, 8, 9 ] ]).

 

List.exists

F#:         let hasEven = [ 1..2..9 ] |> List.exists (fun x -> x % 2 = 0)

Erlang:   HasEven = lists:any(fun(X) -> X rem 2 =:= 0 end, lists: seq(1, 9, 2)).

 

List.filter

F#:         let evens = [ 1.. 6 ] |> List.filter (fun x -> x % 2 = 0)

Erlang:   Evens = lists:filter(fun(X) -> X rem 2 =:= 0 end, lists:seq(1, 6)).

 

List.find

F#:         let fst­Match = [ “cui”; “xue”; “yan” ] |> List.find ((=) “yan”)

Erlang:   Fst­Match = lists:keyfind(yan, 1, [ { cui }, { xue }, { yan } ]).

 

List.fold

F#:         let data = [ (“Cats”, 4); (“Dogs”, 5); (“Mice”, 3); (“Ele­phants”, 2) ]

              let count = data |> List.fold (fun acc (_, x) -> acc + x) 0

Erlang:   Data = [ { cats, 4 }, { dogs, 5 }, { mice, 3 }, { ele­phants, 2 } ].

             Count = lists:foldl(fun({ _, N }, Acc) -> Acc + N end, 0, Data).

 

List.foldBack

F#:         let copy = List.foldBack (fun elem acc -> elem::acc) [ 1..10 ] []

Erlang:   Copy = lists:foldr(fun(X, Acc) -> [X|Acc] end, [], lists:seq(1, 10)).

 

List.forall

F#:         let allEven = [2; 4; 6; 8] |> List.forall (fun n -> n % 2 = 0)

Erlang:   AllEven = lists:all(fun(X) -> X rem 2 =:= 0 end, [ 2, 4, 6, 8 ]).

 

List.iter

F#:         [1..10] |> List.iter (printfn “%d”)

Erlang:   lists:foreach(fun(X) -> io:format(“~p~n”, [ X ]) end, lists:seq(1, 10)).

 

List.length

F#:         let lst = [ 1..10 ]

              // you can call the Length prop­erty on a list

              let len1 = lst.Length

              // or use List.length

              let len2 = List.length lst

Erlang:   Lst = lists: seq(1, 10).

             % you can use the length built-in function

             Len1 = length(Lst).

             % or you can use lists:flatlength

             Len2 = lists:flatlength(Lst).

 

List.map

F#:         let dou­bles = [ 1..10 ] |> List.map (fun x -> x * 2)

Erlang:   Dou­bles = lists:map(fun(X) -> X * 2 end, lists:seq(1, 10)).

 

List.max

F#:         let max = [ 1..10 ] |> List.max

Erlang:   Max = lists:max(lists: seq(1, 10)).

 

List.min

F#:         let min = [ 1..10 ] |> List.min

Erlang:   Min = lists:min(lists: seq(1, 10)).

 

List.nth

F#:         let fourth = List.nth [ 1..10 ] 3

Erlang:   Fourth = lists:nth(4, lists: seq(1, 10)). % node that n is not zero-indexed here!

 

List.partition

F#:         let evens, odds = [ 1..10 ] |> List.partition (fun n -> n % 2 = 0)

Erlang:   { Evens, Odds } = lists:partition(fun(X) -> X rem 2 =:= 0 end, lists:seq(1, 10)).

 

List.rev

F#:         let rev = [ 1..10 ] |> List.rev

Erlang:   Rev = lists:reverse(lists: seq(1, 10)).

 

List.sort

F#:         let sorted = List.sort [ 1; 4; 7; –1; 5 ]

Erlang:   Sorted = lists:sort([ 1, 4, 7, –1, 5 ]).

 

List.sum

F#:         let sum = List.sum [ 1..10 ]

Erlang:   Sum = lists:sum(lists:seq(1, 10)).

 

List.unzip

F#:         let lstA, lstB = List.unzip [ (1, 2); (3, 4) ]

Erlang:   { LstA, LstB } = lists:unzip([ {1, 2}, {3, 4} ]).

 

List.zip

F#:         let lst = List.zip [ 1; 2 ] [ 3; 4 ]

Erlang:   Lst = lists:zip([ 1, 2 ], [ 3, 4 ]).

Share