I notice the Mailbox type is encapsulated and can only be used through the use of the MailboxProcessor.
It implies that to have an agent to which I can post messages, I'm forced to have a single Mailbox of a single type (or use the existing MailboxProcessor in an exotic way).
Should I understand that having multiple Mailbox for a single workflow would inherently result in a bad design? The Ccr clearly gives you that level of freedom.
Edit:
As Daniel pointed out, if one wants to send multiple message types, DUs elegantly solves the problem - and it's not like I haven't done that in the past.
But the question is, isn't doing that a code smell? Wouldn't adding more types of messages sent to an agent over time lead you into have too many responsibilities? I sometimes think it would be important to always encapsulate the message types the agent consumes behind an interface such that this information is never exposed.
I think that F# agents using MailboxProcessor
and CCR implement a different programming model, but I believe that both are equally powerful, although there are definitely problems that could be more nicely solved with one or the other, so it would be nice to have another library for F# built around mailboxes. The programming model based on CCR is probably more clearly described in various languages based on join calculus such as COmega (which is an old MSR project).
For example, you can compare an implementation of one-place buffer using COmega and F# agents:
public class OnePlaceBuffer {
private async empty();
private async contains(string s);
public OnePlaceBuffer() { empty(); }
public void Put(string s) & empty() {
contains(s);
}
public string Get() & contains(string s) {
empty();
return s;
}
}
In this example, the async methods behave like mailboxes (so there are four of them: empty
, contains
, Put
and Get
) and the bodies behave like handlers that will be triggered when a combination of mailboxes contains a value (i.e. when you put into an empty buffer or when you get from a full buffer). In F#, you could use MailboxProcessor
and write:
type Message<'T> =
| Put of 'T * AsyncReplyChannel<unit>
| Get of AsyncReplyChannel<'T>
MailboxProcessor.Start(fun agent ->
let rec empty = agent.Scan(function
| Put(v, repl) -> repl.Reply(); Some(full(v))
| _ -> None)
and full v = agent.Scan(function
| Get repl -> repl.Reply(v); Some(empty)
| _ -> None)
empty )
The two implementations express the same ideas, but in a slightly different way. In F#, empty
and full
are two functions that represent different state of the agent and messages sent to the agent represent different aspect of the agent's state (the pending work to do). In the COmega implementation, all state of the program is captured by the mailboxes.
I guess that separating the state of the agent from the immediate messages that need to be processed might make it easier to think about F# MailboxProcessor
a bit, but that's just an immediate thought with no justification...
Finally, in a realistic application that uses MailboxProcessor
in F#, you'll quite likely use a larger number of them and they will be connected in some way. For example, implementing pipelining is a good example of an application that uses multiple MailboxProcessor
instances (that all have some simple runnning asynchronous workflow associated with them, of course). See this article for an example.
Generally the message type is a discriminated union, which allows for various kinds of messages within a single mailbox. Does that not work in your case?
I don't think you would ever be able to successfully work with a mailbox using only one type of message, unless you use something like the ISubject
type from the Reactive Extensions. Messages come in different forms, and all are important. The two primary examples I can think of are:
- Control messages - denote operations the mailbox should undertake such as clearing its queue, looking for specific messages, shutting down, spinning up child processes, etc.
- Data messages - sending and receiving (Put / Get) are the general types of these.
You are correct in thinking that you would most likely want to restrict the data messages to a certain type, but technically a DU is one type with many alternatives. If you were to go for the same approach as Luca with his initial, dynamic approach in L'Agent, I think both he and I would agree that too many types in one mailbox is a bit of a challenge.
I think I may have found what I was looking for. I have listened to Rich Hickey's talk (Are we there yet) at least 5 times and I believe his approach solves many of the design concerns I had. Obviously this can be implemented with either F# Mailboxes or CAS references.
I really recommend it and would be happy to hear some feedback.