Hey, I just noticed that AQ already made a comment on a related quote to the one I referenced:
There is only one point in the article, at the very end, where I would highlight a slightly different behavior:
The customer, on the other hand, expects a synchronous interaction; he or she came to the store to buy a drink and will have to stay until the drink is delivered. This type of synchronization is fairly common and has been documented as the Half-sync, Half-async pattern.
In the Actor Framework, if both the barista and the customer are actors, then you could design this system with a Reply Msg.lvclass so that the customer is waiting for a reply from the barista. But instead, allow the customer to check his/her cell phone while waiting, or do any number of other tasks (read news paper, talk to friends). The point of the Actor Framework is it lets each actor work on whatever it has to work on. So the customer Actor sends a message to the barista (perhaps using the cachier as a proxy) and then goes off and does its own thing until the customer gets the drink message from the barista. No synchronous waiting needed.
Here I would partially disagre, into that I agree that the potentially long action of waiting for a drink should be async, but that the multiple very-quick steps the Customer performs in ordering the drink should not be individually async. There is a cost to programming things asynchronous (including the poor readability that the OP is concerned about), and that cost is not justified for short (often sub-millisecond) waits.
Wow, thanks for the moment of clarity! This shouldn't have been such a big revelation, but that is totally what I'm doing, using async messages for a synchronous process! I was trying to find another async. way to fix what I felt was wrong, but that was just a code smell for using the wrong technique all-together.
So my main takeaway is that this whole interaction, in my opinion can be seen as two async. interactions (AF Messages). (even though the article talked about it being completely synchronous from the perspective of the customer) Step 1. Order Drink (order, pay, change, etc.), Step 2. Get drink.
I had a similar "Oh wow that made things easier" when I started really using ephemeral actors. Previously, starting and stopping a process was somewhat complicated, so my asynchronous processes would start up at the beginning of my program and shut down at the end. This meant they had to maintain state, report errors back to the thing that called them, synchronize resources, etc.
With AF, I can just launch the actor when I need it, and if it fails to gather resources the actor just dies and alerts the caller. Once the caller fixes the error, it can try to launch again. No state required.
Specific example: a DAQmx actor that read input voltages and continuously streamed them to another actor. Initially, it had "Idle" and "Acquiring" states (among others). Callers would request the actor to start, and if it was already acquiring then it would need to send back an "I'm busy" message, which the caller would need to handle asynchronously (since Reply messages were discouraged). As you see in your example here, it's a giant pain to handle all of that.
So I switched it around- now, my acquisition actor acquires resources in Pre-launch Init. If it can't get them, it just stops, and the caller immediately knows since Launch Nested Actor will return the error- no async needed. If the acquisition actor gets its resources, it just starts streaming data. When you don't need it anymore, you just Stop the actor. If you want to change how things are configured, just Stop the actor and relaunch a new one, which takes virtually no extra time. Now I don't have to manage ANY status messages going back and forth between the two actors. Resource errors are handled in Pre-launch Init, and nonrecoverable DAQmx errors are handled with the regular AF error handling code. A nonrecoverable error will just lead to the actor dying instead of having to try to maintain state and reconfigure something broken into something that works.
Just to flesh this out, this is how I see the Customer's natural state and actions:
But this is an all-async version:
Note that we have added an extra state for the extra step, with all the extra programming burden. This is significantly increased complexity. Imagine if ordering a drink was a ten-step process instead of just two, and we had to add nine extra states!!!
To illustrate the burden, imagine a new message: a Friend actor messages Customer to say "Please change my drink order to a decaf". We have to handle that message appropriately in each possible state. In "Wait for available Cashier" we just change the order; in "Wait for Drink(s)" we just reply "Sorry, drink already ordered"; but what do we do in "Wait for Price"? Ask or a new price, with some kind of mechanism to ignore the yet-to-arrive now-invalid Price message? Lie, and say "Sorry, drink already ordered"? A new "Sorry, I couldn't be bothered to code a retry of pricing" message?
Regardless of what we do, we need to write a new Unit Test that tests proper behaviour for every possible message that could arrive in the 100-microsecond time window that we are in the "Wait for Price" state. Or we accept that our production code will have a potential race-condition bug in it.
Yes, that 100 microseconds could be profitably used to execute important "Look at TikTok on phone" messages, but that is not likely to be worth the extra burden.
I am currently developing a sequencer for a very versatile plugin-architecture that is using a sequencer to run test steps in sequence. Here I had to create a sequencer that is able to dynamically load Actors (function modules) from packedLibs where each of these function modules do not know who is next. The next step is only defined in an XML file that defines the testplan.
These function modules are called by a sequencer actor which simply works through an array of different function modules. When starting a function module it waits until that function module is complete and sends a message back to the sequencer telling him "I am finished". That way there is always only one Actor running.
All function modules inherit from a generic function module class giving them basic functionalities. This allows the sequencer to store all different function module actor objects in a single array that can then be worked through quite easily.
Trick 1 about it is that the sequencer itself is also a function module. This means that a sequencer can call another sequencer which will then call another sequencer which then runs some modules. That way it is possible to have a tree like sequencer with theoretically unlimited depth.
Trick 2 about it is that the sequencer can also be replaced by another sequencer that might not run in sequential order but in parallel order. That way you can run e.g. 3 function modules sequentially and then switch to 4 modules that are executed in parallel. This can save quite some time during production/testing.
Unfortunately this pattern is not programmed that easily - especially when doing it in a plugin based structure. But it is working quite well so far (although development has not been fully finished until now).
Hope this helps you out...