The subject of Actors seems to come up often when discussing Hopac. Both Hopac and actors are models of message passing concurrency, but what are actors and what kind of similarities and differences do actors and Hopac have?
It is somewhat difficult to pin down what exactly actors are, because there are many different incarnations of actors. For example, Erlang is often called an actor language and Akka could be called an actor library. While there are similarities between those two, there are also fundamental differences between them and countless other incarnations of ``actors''.
One thing that is common to many incarnations of the actor model is that the actor concept within them combines both a thread of execution and a message queue or mailbox of some kind. The thread of execution and the mailbox are implicitly tied together so that, within the thread of an actor, a receive statement of some form implicitly takes messages from the mailbox associated with the actor.
To confuse the matter further, the
MailboxProcessor
abstraction, also often described as an actor model, provided with the F# async
model gives an explicit name to the mailbox. However, the MailboxProcessor
model requires that there is at most one concurrent reader of a particular
MailboxProcessor active at any moment. This means that a MailboxProcessor
is effectively associated with a thread of execution.
Unlike those actor models, Hopac and Concurrent ML, which is the inspiration behind Hopac, provide a model in which threads of execution and means of communication are separate similar to the π-calculus. In Hopac, threads of execution are called jobs and various communication primitives, such as, synchronous channels, asynchronous mailboxes, synchronous write once ivar and write many mvar variables are provided. In addition, a form of selective communication called alternatives is provided that allows the expression of first-class synchronous abstractions. The document Programming in Hopac contains some discussion and examples on Hopac programming techniques.
Unlike the way actor models combine a thread of execution and a mailbox into an actor, in Hopac, a single concurrent job may be receiving messages via any number of explicitly named channels—including the possibility of not using any channels—and, on the other hand, a single channel may be used by any number of jobs for receiving messages.
It is often illustrative to give a signature for the essence of a computational model. Let's do that for Hopac and actors.
In Hopac, the threads of execution called jobs and concurrent operations are encoded using monadic combinators. Here is an illustrative signature sketch of the essence of the programming model provided by Hopac:
module HopacModel =
type Job<'x>
val (>>=): Job<'x> -> ('x -> Job<'y>) -> Job<'y>
val result: 'x -> Job<'x>
val start: Job<unit> -> unit
type Ch<'x>
val ch: unit -> Ch<'x>
val give: Ch<'x> -> 'x -> Job<unit>
val take: Ch<'x> -> Job<'x>Using the monadic operations,
>>=*
and
result*,
one can define a thread of execution, represented by the type
Job<'x>*,
and such threads can be started with
start*.
Any number of channels can be created using
ch*
and the channel type
Ch<'x>*
is parameterized with the message type. Within a thread of execution one can
then perform
give*
and
take*
operations on channels. Both give and take operations are synchronous and
block the executing job until the other party of the communication is present
and the operation can be completed.
Please note that the above signature snippet is not a precise subset of Hopac and that Hopac provides a much more comprehensive model as described in the Hopac Library Reference manual.
We could also similarly specify the essence of an actor model. Here is a sketch of such a model:
module ActorModel =
type ActorThread<'a, 'x>
val (>>=): ActorThread<'a, 'x> -> ('x -> ActorThread<'a, 'y>) -> ActorThread<'a, 'y>
val result: 'x -> ActorThread<'a, 'x>
val receive: ActorThread<'a, 'a>
type Actor<'a>
val start: ActorThread<'a, unit> -> Actor<'a>
val self: ActorThread<'a, Actor<'a>>
val send: Actor<'a> -> 'a -> unitIn this model, one can similarly define a thread of execution using >>= and
result. Within an actor thread, one can receive messages via the mailbox of
the actor using the receive operation. Only messages from the single mailbox
implicit to the actor thread can be directly received from. An actor thread is
started using start, which also returns a handle for sending messages to the
actor using the send function. The actor can also use the self operation to
get a handle to itself. The send operation is asynchronous and does not
block. If the destination of a send operation is not immediately ready to
receive the message, then that message is queued to the mailbox of the actor.
Please note that the above sketch of an actor model does not attempt to
precisely capture any specific actor model such as Akka or Erlang. Like the
actual model provided by the Hopac library and the model specified in the
HopacModel signature, the above ActorModel is also drastically simpler than
many actual actor models.
It is often instructive to see how one model of computation can be encoded in another model of computation. Here is an encoding of the above actor model using Hopac:
module ActorModel =
type ActorThread<'a, 'x> = AT of (Ch<'a> -> Job<'x>)
let unAT (AT x) = x
let (>>=) (xA: ActorThread<'a, 'x>) (x2yA: 'x -> ActorThread<'a, 'y>) : ActorThread<'a, 'y> =
AT (fun aCh -> unAT xA aCh >>= fun x -> unAT (x2yA x) aCh)
let result (x: 'x) : ActorThread<'a, 'x> =
AT (fun aCh -> Job.result x)
let receive : ActorThread<'a, 'a> =
AT (fun aCh -> upcast Ch.take aCh)
type Actor<'a> = A of Ch<'a>
let unA (A aCh) = aCh
let start (uA: ActorThread<'a, unit>) : Actor<'a> =
let aCh = Ch ()
Hopac.start (unAT uA aCh)
A aCh
let self : ActorThread<'a, Actor<'a>> =
AT (fun aCh -> Job.result (A aCh))
let send (aA: Actor<'a>) (a: 'a) : unit =
Hopac.start (Ch.give (unA aA) a)As can be seen above, it is fairly straightforward to encode an actor model
using only a small subset of Hopac. (Note that the >>= operation used within
the definition of >>= is the Hopac bind operation.)
Please note that the above is not meant to demonstrate a practical way to do
actor style programming in Hopac. The above is meant as an illustrative
encoding that hopefully helps to understand both models. The above encoding is
not very practical, because it is very inefficient in a number of ways and it is
possible to implement actor models and program in actor model like styles
directly within Hopac. While the above uses the actual Hopac library, so that
you can directly compile the above code, it makes no use of any primitives in
Hopac beyond what is in the previous HopacModel signature.
For completeness, here is the reverse encoding, an implementation of the
HopacModel using the ActorModel:
module HopacModel =
open System.Collections.Generic
open ActorModel
type Job<'x> = J of ActorThread<unit, 'x>
let unJ (J x) = x
type Msg<'x> =
| Take of Actor<unit> * ref<option<'x>>
| Give of Actor<unit> * 'x
type Ch<'x> = C of Actor<Msg<'x>>
let unC (C x) = x
let Ch () : Ch<'x> =
let givers = Queue<Actor<unit> * 'x>()
let takers = Queue<Actor<unit> * ref<option<'x>>>()
let rec loop () =
receive >>= function
| Give (giver, x) ->
if takers.Count > 0 then
let (taker, r) = takers.Dequeue ()
r := Some x
send giver ()
send taker ()
loop ()
else
givers.Enqueue (giver, x)
loop ()
| Take (taker, r) ->
if givers.Count > 0 then
let (giver, x) = givers.Dequeue ()
r := Some x
send giver ()
send taker ()
loop ()
else
takers.Enqueue (taker, r)
loop ()
C (start (loop ()))
let give (xCh: Ch<'x>) (x: 'x) : Job<unit> =
J (self >>= fun giver ->
send (unC xCh) (Give (giver, x))
receive)
let take (xCh: Ch<'x>) : Job<'x> =
J (self >>= fun taker ->
let r = ref None
send (unC xCh) (Take (taker, r))
receive >>= fun () ->
match !r with
| None -> failwith "Impossible"
| Some x -> result x)
let (>>=) (xJ: Job<'x>) (x2yJ: 'x -> Job<'y>) : Job<'y> =
J (unJ xJ >>= fun x -> unJ (x2yJ x))
let result (x: 'x) : Job<'x> =
J (result x)
let start (uJ: Job<unit>) : unit =
start (unJ uJ) |> ignoreThe above encoding is obviously much more complex than the previous encoding of
the ActorModel using Hopac. Of course, as discussed previously, the
HopacModel is only a small subset of Hopac and does not include the
alternative mechanism. Similarly the ActorModel is only a small look-alike
subset of what many actual actor models provide.
A merit of many of the actor models is that due to the marriage of threads and mailboxes within those models there are often idiomatic ways to structure programs. The model provided by Hopac, due to the separation of threads and channels, often allows for many more ways to structure computations and while such flexibility can be advantageous it can also be overwhelming. In this section, we'll consider how some actor style idioms might translate to practical programming styles in Hopac.
Let's start by implementing something similar to a subset of the F# MailboxProcessor within Hopac. We'll just define an actor as an asynchronous Mailbox:
type Actor<'m> = Mailbox<'m>To start an actor, we use a helper function that constructs a job that creates a mailbox and starts a job with that mailbox:
let actor (body: Mailbox<'m> -> Job<unit>) : Job<Actor<'m>> = Job.delay <| fun () ->
let mA = Mailbox ()
Job.start (body mA) >>-. mAWithin the body of an actor, the actor can simply receive messages from the mailbox. Let's make that a bit more concrete:
let receive (mA: Mailbox<'m>) : Job<'m> = Mailbox.take mAFor sending messages to an actor started with the help of actor, we can simply
use the operations provided for mailboxes. But let's make that a bit more
concrete:
let post (mA: Actor<'m>) (m: 'm) : Job<unit> = mA *<<+ mTo allow an actor to provide a reply to a message, we can, similar to
MailboxProcessor, send the actor a message passing object of some kind. In
Hopac we could use one of many different message passing objects. Closest to
the AsyncReplyChannel
would be an
IVar*:
let postAndReply (mA: Actor<'m>) (i2m: IVar<'r> -> 'm) : Job<'r> = Job.delay <| fun () ->
let i = IVar ()
mA *<<+ i2m i >>-. iTo reply to a message, the actor then needs to write to the given IVar:
let reply (rI: IVar<'r>) (r: 'r) : Job<unit> = rI *<= rConsider the following echo actor:
type Echo<'x> = Echo of 'x * AsyncReplyChannel<'x>
let echo () = MailboxProcessor.Start <| fun inbox -> async {
while true do
let! Echo (x, xArc) = inbox.Receive ()
do xArc.Reply x
}Using the previously defined combinators, we could express a similar actor as follows:
type Echo<'x> = Echo of 'x * IVar<'x>
let echo () = actor <| fun inbox -> job {
while true do
let! Echo (x, xI) = receive inbox
do! reply xI x
}One difference in the MailboxProcessor like operations implemented here is
that the Hopac operations return jobs that then need to be run. This is
intentional. The implementation of Hopac is such that the concurrent operations
within Hopac run fastest when they are executed by worker threads of a Hopac
scheduler. Running individual concurrent operations outside of workers incurs
potentially significant overheads. Therefore the operations sketched here
return jobs that can then potentially be composed into longer jobs to run.
This is just a small sketch and omits many features of the MailboxProcessor.
You could continue extending these snippets to quite straightforwardly add
support for most of what the MailboxProcessor provides, including features
such as error events. In most cases, however, there is no practical need to
work like that. While the above actor and postAndReply functions could be
handy shortcuts in some cases, most of the other operations are readily
available within Hopac or similar functionality can be expressed more flexibly
and possibly more concisely. For example, here is a more concise implementation
of the above echo example using operations directly available with Hopac:
type Echo<'x> = Echo of 'x * IVar<'x>
let echo () = actor <| fun mb ->
Job.forever (mb >>= fun (Echo (x, xI)) -> xI *<= x)Aside from being more concise, this version is also likely to be faster.