I'm not sure what more generality you could want (assuming that the above code transformation is correct now). Applying it to general monads is the easiest step.
So, I'll try again.
In general, if you have some function with an effect (indicated by the keyword you have to use), you would call flatMap:
func foo(_ x: X) arbitrary -> Y{
...
y = arbiTry bar(x)
...
}
is
func foo(_ x: X) -> Arbitrary<Y>{
bar(x).flatMap{localX in ...}
}
Any variable above flatMap that is needed below would have to be captured.
The problem you describe arises when the effect is thrown from some local scope, like some while clause:
func foo(_ x: X) -> Arbitrary<Y>{
while ... {
bar(x).flatMap[//indicates what the scope *should* be
}//ouch
]//flatMap only ends after while
}
As indicated above, one would have to re-write the while loop, possibly in a recursive way. Generalizing your above code example:
func foo(base: Int) -> Arbitrary<Int>{
var result = base
if result < 0{
arbitraryThrowing().flatMap{//needs to contain *all* branching logic of while
result = $0//could be optimized by just passing $0 to arbitraryCaller
return arbitraryCaller(base: result)//recursion instead of while loop
}
}
else{
return .pure(result)
}
}
I am sure if we discuss this long enough, we figure out the general rules for all possible code transformations. Swift doesn't have too many control flow statements, and they can partially be reduced to other control flow statements. I have no idea how to actually write code that does code transformation, however I think even I could figure out a correct set of rules to do that in a way that it can be actually implemented. I mean, other languages (Eff!) have done something very similar.
To reiterate: monads are nothing else than continuations with type-specific invocation semantics. Any code you could write to make async/await syntactically possible (and it really only is syntax, not semantics) just needs to be slightly generalized in order to make really arbitrary monadic effects possible - and then you have a huge extensible feature almost for free.
There is no inherent difference between what needs to be done to make your above code example work and making, e.g., the following work:
var result = -1
while result < 0{
result = log foo()//writer monad
}
return result
or
var result = -1
while result < 0{
result = read foo()//reader monad that injects implicit arguments to a function
}
return result
or
var result = -1
while result < 0{
result = maybe foo()//optional monad. enclosing function may or may not return a value, depending on the success of this call to foo
}
return result
or
var result = -1
while result < 1{
result = interpret foo() //Free monad that can be used to inject arbitrary semantics
}
return result
All of these have an obvious operational semantics that could be achieved by having the compiler rewrite that a little bit - but structurally in the same generic way.