F# features the imperative model (which means: I want you to run this code now, even if I don't care about the result.). IT's not a good choice. Only the functional model is good (which means : now I need the result in output, execute all what you can do to return this output).
But imperative conditions are in fact extremey rare. What you atually need of a program will be a final output. This output depends only on preconditions that will need to be met to get it. All preconditions specified in a programe need NOT be evaluated immediately, but only if the ouput participant MUST run (when you initially launch a program, only ONE rendez-vous must be met and is candidate for execution : the END of the program when it has produced that output. All the other execution points are delayed (they may be ready to run wut will only run when it is a participant to a rendez-vous whose SINGLE output has been made runnable.
In a normal program (or algorithm) you absolutely don't need that every step in a sequence have been executed. In fact before ordering the imperative execution of this program, your rendez-vous is inactive but just wants to be able to do type inference to see if you need to activate one of the input branches of the rendez-vous. So the actual execution of the program will first consiste in preparing the execution context of the entry point of this sequence, the sequence being still not executed, allowing you to transform that context before effectively making it run by imperatively wanting the output of that sequence.
Most elements in a program are NOT a sequence, unless there are preconditions for one of them depending on the result of another one, they are candidates for running ALL concurrently, in any order. This means that all these concurrent execution paths are not only not executed when the rendez-vous is first entered (but not exited with its output path), but they are all separate members of a single rendez-vous object, which will activate the input branches only when the output branch will have been activated.
The process of activating branches, and propagating the activation of branches in REVERSE execution order, or in PARALLEL upwards branches, is the job of the run-time execution machine (i.e. its compiler and optimizer). This machne will decide itself the resources it can use to activate as many branches as possible, creating parallel execution contexts if possible (according to performance monitoring and resource constraints like the available memory), or sequencing them in arbitrary order, up to the point that each input branches have been activated and then met the rendez-vous.
Programs should never need to specify an imperative execution path. In almost all cases, this should only be specified by the set of preconditions, i.e. by the specification of the rendez-vous which is crossed in two steps : on entry to prepare the strongly typed execution context of each input branch (allowing type inference at theis step), and on exit when type inference has ordered all input branches to run because all input preconditions were satistied and because the output branch has been activated.
An example is I/O : this can be modeled like a client/server model : there's a server waiting for input and, so, which is implementing an event listener. Its precondition is: an event has been received. But to receive this event, the server must activate its input branches of its rendez-vous (in any order, or concurrently, this does not matter). One of these branch will be the output branch of a client (local or remote), which is still inactive as long as the server has not activated it. So it's the rendez-vous that tells the client that its ouput is desired.
For asynchronous client events, basically the server implements an loop around its rendez-vous : many clients may be connected to this rendez-vous, and under this definition, they can (and will) run in parallel asynchronously, because they will all be activated : the server when exiting a looping rendez-vous to retun a result to an output branch, does not actually exit the loop, it stays in that rendez-vous and clients remain connected to them, up to the point where their output has been processed by one of the parallel output branches.
In summary: even an ouput order is not imperative : the output completion however is one of the preconditions needed to terminate an input operation. You can see here the concept of "I/O pipes". If you need an ordered output, what you create is a sequence, i.e. a chained suite of preconditions (output completion events), and this branch has a single entry point and a exit point (only the activation of the exit point is needed, the entry point will be reached by cascading/ propagating the activation event upward from the exit point to the entry point, in the chain of precondition rendez-vous). Everything in this model is correctly ordered by dependancy, everything is parallelizable at each rendez-vous level, and sequences are the result of chaining preconditions.
No programmer should need to specify imperative execution models, except in terms of sequences (basically, only to perform ordered output from arbitrarily ordered and parallelizable inputs).
Serializing inputs is also possible using preconditions, by chaining the inputs in sequences. But like sereilized ouputs, serialized inputs are not all activated at the same time, the activation will start from the final rendez-vous of the chain and will propagate up. Here also input can be performed in a loop: a single input in the rendez-vous from the input, but a loopback of the outputas an input of the same rendez-vous, whose precondition is says that all the input branch must be in the ready-to-run state : exiting the rendez-vous by taking one input value means that the initial branch is no longer active, but the loop back will reactivate it to process the nest input value.
In other words: input precondition should include in the rendez-vous the completion status of an ouput, and the I/O reverse is true.
input and output rendez(vous are not different, both can be chained to perform serialization or to implement client-servr dialogs. But there are three kinds of rendez-vous (in fact only two):
- the sequence (with one input branch, and one output branch, the first that will be activated)
- the loop (whose output branch is connected by some execution paths as an input branch of the rendez-vous)
- classical rendez-vous with multiple input branches are just like the sequence, except that this is generalized to mutliple input branches instead of just one.
In a pure functional program, the parallel execution model is the default, they imply the generation of a single rendez-vous that will be crossed only when all input branches have been activated (they can all be activated at the same time, waiting then from their concurrent completion events) : you should not need any separator in the programming language other than a simple space to separate items (each only representing an input of thr rendez-vous). You activate then the output branch of this rendez-vous, which means that type inference will start there according to the execution context of each (parallel) input branch. This is the step where a compiler can optimize things and elliminate input branches that are always complete without needing any execution (type inference can be performanded once if the type has already been seen in such context).
The compiler will determine itself how many input branches can be activated at the same time, in an arbitrary order acdording to various constraints, creating parallel threads or launching remote execution if available and appropriate for the monitored performance (allowing scalability), or running them in sequence if not possible. A program can still specify a higher priority between these parallel branches, but should not block them.
To insert a rendez-vous between the parallel inputs (that are just space-separated) the simplest way is to insert a semicolon between them :
'a b' is a set of two parallel execution branches, it has no rendez-vous, it cannot run.
'a b;' activates the rendez-vous at end of the execution paths of the parallel branchs a and b. At this point type inference occurs and if the output of 'a' is a function, it means that you'll need the code of this function, and to pass the lazy value of 'b' to this code. Both branches are made "runnable" but are still not activated to run. The compiler can determine which branches a or b to run first or can run them concurrently. According to type inference results, 'a' is a function and its code which was lazily avaluted needs to be first evaluated to return an instance of its code to which it will be passed the lazy value of its first parameter 'b'. Then type inference has determined that the result of the evaluation of this code with 'b'. The evaluation of 'b' wll be made by this code only when it will need its actual value. The result is then, by type inference another function without parameter, whose code will return a value of some type. But the ";" rendez-vous instructs that function to return its value by activatng it on the input branch of the rendez-vous, which causes the two branches to be activated in parallel. then the code of 'a' to act with 'b' when it will be needed by activating it. As both input branches of the rendez-vous have been activated it is possible to exit this rendez-vous: the result is that function which will operate according to the definition of 'a' and the value of 'b' if it needs it.
- "a; b;" is the typical sequence, here with two chained rendez-vous. both branches a and b are inactive but only the second rendez-vous is activated at start. The sequence creates an execution context that is the result of this chaining of rendez-vous.
On other words, this basic language creates a graph of rendez-vous.
You can add extra syntax to create the other type of rendez-vous, the loop: 'a; {b}; c' whch has a single entry, a precondition on the output of 'a' connected as an input of the rendez-vous the ouput of 'b' as another input of the same rendez-vous, and 'c' representing some code that will run in sequence after the loop. 'a', '{b}', 'c' are all separate and concurrent execution paths, they would be all active at the same time, if there was no ';' to chain them in a sequence. But the first ';' is a basic sequence, the second one afer the '}' is a loop rendez-vous chained to a sequence.
But normal programs will never need the sequence in a purely functional program in a "timely" fashion. It is the type inference and chaining of rendez-vous that orders the execution in an imperative way. Solution: drop the ";", it is not needed at all. Instead, if you need it write the code for "c" in "a {b} c" so that it will include a rendez-vous depending on some properties (completion status, infered type, return calue..) from another object of this program, here 'b" and or 'c'. Typically the code of b should be enumerating the content of some collection (including a basic range of integers) to create as many parallel instance of some code for each enumerated value These parallel instances will also run in parallel to 'a' and 'c'.
If you need some ordering, use explicit preconditions to chain the rendez-vous.
Now how do you write preconditions ? just like traditional if statements:
(a ? b : c)
i.e. if precondition from a is met then b is activable, otherwise c is activable. A rendez-vous is created on output of a, b and c, all three are activable but the activation of the rendez(vous input from 'a' will be the only one occuring if the output of the rendez-vous occurs. Forcing 'a' to be evaluated to get its default boolean property. Depending on this value the "?" operator will activate b or c and will complete immediately the rendez-vous completion of the other branch without execuring it. Beause 'a'; and either 'b' or 'c', is now complete, the only remaining active branch is the code of 'b' or 'c' depending on the boolean property value of 'a'. Actually the ':' is an operator/function here which creates a function that will activate either 'b' or 'c', and the '?' is an operator/function which operates on 'a' and the function 'b:c'. This could as well be written as '(? a : b c)', more like a switch. And parentheses are just there to limit the scope of operands for type inference.
The ';' is just a shortcut for another rendez-vous : 'a ; b;' is the same as
'(? (a).complete b);'
using the reverse polish notation, or
'((a).complete ? b);'
using the more common infixed operator notation which could be infered as well from type inference, The 'complete' property returns the completion event which will always be true when the activation of 'a' will have been made so that its execution will have reached the completion status to reach the first input of the rendez-vous; the second rendez-vous is built from the completion of b which is activated, but not runnable because it is connected on output of the first rendez-vous and depends on this first rendez-vous to be met.