This answer is roughly in the context of your current approach rather than some alternate totally different approach.
First, your solution has some problems as it appears. Your solution would make more sense if pass 1 was concurrent with pass 2, rather than a sequential series of passes. Instead, consider what would happen if you ran actions
twice sequentially, i.e. combined them sequentially in a larger computation expression: first pass1Action
would execute, then it would stop waiting for pass 2. When pass 2 came, pass2Action
would execute, then the second instance of pass1Action
would execute, then it would stop waiting for pass 2 to come again (which presumably it never will(?)).
Essentially, the problem is you are treating pass 2 actions as being synchronously callable from pass 1 which doesn't make sense: pass 2 actions can't return values to pass 1. Calling a later pass from an earlier pass should be viewed as a non-returning asynchronous call.
You can correct this and simplify your implementation (via not needing to use continuations) by using the Writer or Output monad. Basically, you add an operation runInNextPass
(or even runInPass
which takes which pass to target as a parameter). Your code then looks like the following:
let actions =
passHandler {
let myObject = pass1Action()
runInNextPass pass2Action
}
runInNextPass
is then just tell
or a slight variant thereof from the article. Executing the computation as a whole would just be running it as normal, getting a Writer
value out, then running the output value. This works well if you only want to run exactly N passes for a known (but possibly only at runtime) number of passes: just repeat the pattern of running N times. (In its N=2 form, this is a handy pattern when you want to run some code that needs an initialization phase to happen before a "real" execution phase.) However, just by changing the monoid we're using (see the article), we can get a couple different effects.
The scenario above corresponds to having the output of the Writer
monad, i.e. the monoid, being either the side-effecting actions (or an IO monad) represented as functions of type () -> ()
or the Writer
monad itself. Each layer of Writer
type constructors corresponds to an extra pass. So Writer<() -> (), 'a>
represents a 2-pass system, Writer<Writer<() -> (), ()>,a>
represents a 3-pass system and so forth. In this case, the number of passes would be statically enforced, you couldn't use a pass 2 operation (type Writer<() -> (), ()>
) in pass 3 (operations of type () -> ()
). If you want a not-statically-known number of passes, then you make the recursive type type Pass<'a> = NextPass of Writer<Pass (), 'a> | FinalPass of () -> 'a
. If F# supported higher-kinded polymorphism, you could write something like
type GPass<'f, 'a> = NextPass of Writer<'f<GPass<'f, 'a>>
| FinalPass of () -> 'a
The benefit of this is that we can change which monoid we are using. For example, when 'f
is Option
, then we can have a sequence of passes that run until no later passes are requested. When 'f
is a variant of Map
you can have passes that are labeled so your "parse" pass could directly request work to be done in the "optimization" pass without having to know how many passes are in-between. In this approach though, you may want to pass around information on which passes have already been run (possibly by adding a Reader or Environment monad aspect), because there is nothing stopping the "optimization" pass from requesting work to be done in the "parse" pass.