Pondering Axum + F#
It’s been a while since I’ve posted about Axum as I’ve been posting about other asynchronous and parallel programming models. After two releases of the Axum standalone language and lots of good user feedback, it’s time to ponder what could be with Axum. Overall, the goals of Axum to provide an agent-based concurrency oriented system is important as we consider the emerging hardware trends. Many of these ideas would in fact benefit most languages, whether mainstream or not. With all the success that it has had, there have also been some issues as well, which leads me to wonder, what else could we do here? Let step through some of those issues today and see where we could go.
Should It Be Its Own Language?
One of the highlighted concerns was the need to have yet another language. Creating and maintaining languages is a large effort, not to be undertaken lightly at any organization. To base your language on C# while adding additional constructs was a great way to get started and create some buzz and buy-in, but it also brought its own set of problems.
One problem is that it is an enormous effort to keep on par with the C# compiler and add these constructs on a continuing basis. The Spec# project, also based upon C# realized that it wasn’t feasible to keep up with the rapid pace of the language and instead went as a language neutral approach. Not only keeping up with the language, but to enforce many functional programming constructs that Axum embraces such as immutability and purity are harder to enforce given a language which is built upon object-oriented and imperative approaches, an approach which encourage the encapsulation and manipulation of state. So, instead of creating a separate language, why not fold these constructs into an existing language instead?
The Case For F#
Given the impedance mismatch between the C# language and the overall goals of Axum for a safe, isolated, agent-based system that is great for concurrency, why not consider another language such as F#? Now that F# is a first-class citizen within Visual Studio going forward, there can be a strong case made. Given some of the features of F#, which we will discuss, could potentially be a happy marriage with Axum. The F# language and its associated libraries have many of the constructs that are essential to an agent-based messaging system, including immutability (albeit shallow), encouraging side effect free programming, and library functions such as the mailbox processor messaging classes.
In Praise Of Immutability
In the world of concurrency we find that shared mutable state is evil. Often we have to protect certain parts by using low level constructs such as locks, semaphores, mutexes. Because of this, we find that most people tend to avoid concurrency programming as it tends to be too hard to get just right. Instead, communication through message passing in languages such as Axum and Erlang, immutability is the standard. This way, we can freely reference the values without any worry of whether they will change underneath us. Providing first-class support for immutability goes a long way towards making safe concurrency programming easier for the masses. With the F# language, we get that approach by default and with rich types such as records, discriminated unions, lists, tuples and more.
Of particular note, Discriminated Unions are quite useful in messaging systems to define the types of messages your system accepts. For example, we could model our auction messages quite easily using discriminated unions to define which interactions are legal, such as the following code:
type AuctionMessage = | Offer of int * MailboxProcessor<AuctionReply> | Inquire of MailboxProcessor<AuctionReply> and AuctionReply = | Status of int * System.DateTime | BestOffer | BeatenOffer of int | AuctionConcluded of MailboxProcessor<AuctionReply> * MailboxProcessor<AuctionReply> | AuctionFailed | AuctionOver
In this case, we have succinctly defined what messages can be processed. Adding to this, Axum, through the use of data-flow networks and channel protocols could be a nice compliment to determine how these messages are processed. Where else could they match up nicely?
Built For Concurrency
To talk about a potential marriage between F# and Axum, we need to talk about the language and its libraries and how they support concurrency. From first class events, to asynchronous workflows, to mailbox processors, F# has a wide array of asynchronous and parallel programming libraries available out of the box. In particular, the asynchronous workflows and the mailbox processor could match well with the Axum programming model. Interestingly, Luca Bolognese has built upon the mailbox processor for his LAgent framework to show some more interesting scenarios where agent based programming could work. This includes such things as advanced error handling, hot swapping, implementing MapReduce and so on.
With little effort, we can model such things as the standard ping-pong example using F# mailboxes:
let (<--) (m:MailboxProcessor<_>) msg = m.Post(msg) type PingMessage = Ping of MailboxProcessor<PongMessage> | Stop and PongMessage = Pong let ping iters (pong : MailboxProcessor<PingMessage>) = new MailboxProcessor<PongMessage>(fun inbox -> pong <-- Ping inbox let rec loop pingsLeft = async { let! msg = inbox.Receive() match msg with | Pong -> if pingsLeft % 1000 = 0 then printfn "Ping: pong" if pingsLeft > 0 then pong <-- Ping inbox return! loop(pingsLeft - 1) else printfn "Ping: stop" pong <-- Stop return () } loop (iters - 1)) let pong () = new MailboxProcessor<PingMessage>(fun inbox -> let rec loop pongCount = async { let! msg = inbox.Receive() match msg with | Ping sender -> if pongCount % 1000 = 0 then printfn "Pong: ping %d" pongCount sender <-- Pong return! loop (pongCount + 1) | Stop -> printfn "Pong: stop"; return () } loop 0) let ponger = pong() let pinger = ping 100000 ponger pinger.Start() ponger.Start()
And you’ll notice from all of this, there are no mutable values anywhere nor shared global state and all communication happens through message passing. We have certain limitations here such as we cannot remote these calls, so all interaction happens inside the single application. Not only that, but how do we enforce no shared state?
What Would It Look Like?
So, now that we looked at some of the reasons why F# and Axum might be a good marriage, what might it actually look like? That’s a great question! Some of the concepts from Axum could map perfectly to F# whereas some might need a little more coaxing. Let’s go back to the basic building blocks of Axum. They consist of the following:
- Agents and domains
- Channels
- Schema
- Networks
The first three items are concerned mostly with state isolation and the exchange of information between our isolated regions of our application, while the last item deals more with the how messages are passed.
The domain is the most basic isolation concept where all data is encapsulated and only constructors are exposed outside of the domain definition. Agents are defined to run within the isolated space of a domain with each agent on a separate thread of control. These agents are exposed to one another through passing messages back and forth via channels which define ports through which our data flows. The data that passes between our domains can be defined in a schema, similar in nature of XML Schemas, to define structure and rules of our data. Now to think about message passing, F# already has asynchronous behavior already built in to the mailbox processor, so we could take advantage here and build on top of it and our networks could guide us as to what order messages are passed.
Let’s look at some possible code examples of what things might look like. I’ll choose just a couple as they might work nicely out of the box. Let’s first look at how we might define a port which accepts a given message.
type ProductInput = Multiply of int * int | Done type ProductOutput = Product of int | AckDone
This way, we could define what our payload type for both incoming and outgoing. Another area would be to look how schemas might be done. In our earlier example, we used simple data types such as integers, but what about more complex types? Using F#, we could think of using records here to simulate the not only the data, but what is and is not required:
type PersonSchema = { Name : string Employer : string Age : int option } let matt = { Name = "Matthew Podwysocki" Employer = "Microsoft Corporation" Age = None }
By marking the fields we require as standard fields and those which are optional as option types works out nicely. Using F#, we’re able to have concise syntax with a language that is already defined and quite flexible.
Conclusion
As you can see from this post, a potential marriage between Axum and F# would certainly be a great fit. Instead of focusing on Axum as a separate language, focusing the effort on top of an existing language which matches many of the design goals can bear a lot of fruit. I believe Axum is important for the .NET programming stack, giving us more options on how to deal with concurrency and changing hardware architectures. What do you think?