Actors in F#


It’s been a while since I posted anything here, mainly because we’ve been busy on the Axum team blog over the last few weeks and months. Inspired by this post by Matthew Podwysocki, I thought it would be interesting to show actors in F#, which are closely related to the Axum model.

PingPong is a fairly common micro-benchmark for message-passing: it is used because it measures the pure overhead of passing messages, with the logic in the actors doing no interesting work. It is a trivial piece of code, but very educational in that it shows a very simple framework for creating actors that go back and forth synchronizing their work. It is easy to expand on, etc.

Matthew’s blog shows the Erlang code and then the Axum code. Let’s also consider F#, which like Erlang has built-in support for actors, and comes from a functional tradition. The PingPong example looks like this in F#:

open System

type message = Finished | Msg of int * MailboxProcessor<message>

let ping iters (outbox : MailboxProcessor<message>) =
    MailboxProcessor.Start(fun inbox -> 
        let rec loop n = async { 
            if n > 0 then
                outbox.Post( Msg(n, inbox) )
                let! msg = inbox.Receive()
                Console.WriteLine("ping received pong")
                return! loop(n-1)
            else
                outbox.Post(Finished)
                Console.WriteLine("ping finished")
                return ()}
        loop iters)
            
let pong () =
    MailboxProcessor.Start(fun inbox -> 
        let rec loop () = async { 
            let! msg = inbox.Receive()
            match msg with
            | Finished -> 
                Console.WriteLine("pong finished")
                return ()
            | Msg(n, outbox) -> 
                Console.WriteLine("pong received ping")
                outbox.Post(Msg(n, inbox)
return! loop() } loop()) let ponger = pong() do (ping 10 ponger) |> ignore

The structure and amount of code in this example is very similar to the Erlang code and is similarly hard/easy to read, depending on your perspective.

The mailbox processor design follows the traditional actor-oriented pattern and is therefore a bit different than the approach we’re taking in Axum. Notice that rather than establishing a channel between ping and pong, we’re sending the mailbox to the other side with each message.

One of the advantages of this model over the Axum model is that any number of clients can communicate with an actor, all that is needed is access to the mailbox reference. This means that one single pong actor can service the requests of many ping actor, at least if the implementation is completely stateless.

The mailbox model also has some disadvantages: it is hard to reason and orchestrate the interactions between n client actors when you cannot distinguish them, and any hint of statefulness ruins the scenario. In the pong case, the state we have is whether the ‘Finished’ message has been received. Once one pinger sends it, pong will stop servicing others, who will never know. In the channel model, there’s an explicit agreement between only two parties on how they both will behave, an agreement that can be checked at runtime and, in many cases, at compile time.

Another annoyance is that mailboxes do not have any clear endpoints – I can create a mailbox and start receiving messages from it. With the channel model, each side of the channel is unique and the type system takes care of separating them: you send from one and receive from the other, and ne’er the twain shall be confused.

As you can tell, I have a certain bias, but that doesn’t mean I’m down on the F# model. In fact, I love F# and hope there is a way we can combine its conciseness with the, in my humble opinion, safer Axum model for channel-based communication.

You should download and try Axum, but do also play with F# and its actor-based API. I’d love to hear your analysis of the relative strengths and weaknesses of the two models.

Niklas


Comments (8)

  1. Thank you for submitting this cool story – Trackback from DotNetShoutout

  2. &#160; In the previous post , I gave the canonical Ping-Pong example in Axum and how it compared with

  3. In the previous post , I gave the canonical Ping-Pong example in Axum and how it compared with Axum.

  4. A bit of a slow week.&#160; Perhaps some are out playing in the recently fantastic weather instead of

  5. John.Koepi says:

    Hi all. Thank you very much for your f# actor’s sample. It’s very interesting.

    I have faced one suspicious issue when playing with your code example. I use single core notebook Asus Z99L. When i have ran your code, the only message i saw was: Press any key to continue…

    So, i guess when main thread dies, all actors dies too at once. So the strange moment is that main thread dies right after programm starts. Of course, it dies after "do (ping 10 ponger) |> ignore" code line because programm ends. Is it normal?

    So, if you add following code to the end of your example: "System.Threading.Thread.Sleep(100)" and change number of pingpong iters to 1000, you will see maximum number of pingpong iters that the actors can do on your computer during 100 ms time range.

    Have you any comments on this?

    Best Regards, Ivan, Russia

  6. Garry Trinder says:

    Ivan,

    You’re right — the code I posted is missing a final ‘do Console.ReadLine()’ that will keep the process alive until the work finishes. That’s what I actually used to run the code myself, but somehow it was omitted in the code above.

    Niklas

  7. My fellow programming language enthusiast Matthew Podwysocki just posted another F#-based actor sample