Computation blocks - IS4Code/Sona GitHub Wiki
Computation blocks offer a general way to extend the capability of statements and expressions within a particular scope, by interpreting these operations using a particular workflow. Depending on the design of such a workflow (represented by a pre-existing builder object), simple code may express complex semantics such as asynchronous continuations, parallelism, option binding, or any other sort of monadic operation.
There are several special statements whose use is facilitated in computation blocks, namely:
follow‒ generalized "await". Also available in combination with individual other statements as a pseudo-operator.yield‒ outputs a value out from the computation. Also related toyield returnandyield break.with‒ used to "enter" a computation block, using its argument as the new builder. Also in statementsfollow withandyield with.- The builder may also define any other operations, which are exposed as custom unqualified statements, and is also responsible for interpreting statements like
return,try,while, etc.
Computation modes
Computations operate in one of two modes, single-valued or sequence-like, and may utilize either the default (global) builder, or any existing object, given as the argument to with.
The single-valued mode is the initial mode of any isolated block of code (a package, function, inline statement, etc.), and has the following properties:
returnis supported in all positions, stopping execution when used and providing the final value of the block.- If no
returnis encountered by the end of the block, an implicitreturnis provided with an empty value (eitherunitornonewhen inside an optional function). yield breakis supported only as the final statement in the block and prevents the implicitreturn.
The sequence-like mode is enabled by {…} and […] used for collection construction, after with.., and in iterator functions:
yield breakis supported everywhere to terminate the sequence. Its use is implicit at the end of the block.returnis not supported. To actually use the builder's implementation,yield returnmust be used instead, but it does not stop the execution.- If no custom builder is provided, the block results in an object of type
..(a sequence), lazily executed.
Importantly, the mode of execution itself does not affect whether return, yield, or follow is available in the first place, as this is what the builder object decides. By default (or after with() or with..()) the default (global) builder is used, which defines follow for a few common types, such as:
- For asynchronous operations, it pauses the current thread until a value is produced.
- For option-like objects, it unwraps the value or throws an exception if there is none.
- For sequences, it expects a single element and retrieves it.
Note that these operations (implemented by calling Sona.Runtime.Computations.global.ReturnFrom) are meant to be used only for exposing values from computations in top-level code, as the side effects may otherwise be undesirable.
follow
The follow keyword is usable both as a single statement, and as a part of some additional statements, to "unwrap" its sole argument in a way determined completely by the workflow previously entered via with. This generally corresponds to evaluating any operations left to be performed by the argument, waiting for their result, or exiting early if no result is present.
The name of this keyword is modelled after "await" in other languages, but meant to denote an action that is deliberately vague in terms of spatial, temporal, or existential definiteness. Unlike "unwrap" in some languages, follow "always succeeds" since situations that may be considered "exceptional" (such as a value not being present) are nonetheless valid (and handled by the builder).
There is no guarantee that any remaining code after follow executes immediately or even at most once. This is because the code is internally wrapped in a separate function and given to the Bind operation on the builder object, together with the argument of follow, leaving it to the builder to decide when and how many times the function shall be called.
Syntax
follow_statement:
'follow' expression;
follow_discard_statement:
'follow' member_expression '!';
follow_expression:
'follow' unary_expression;
Each version of follow is used for a different purpose:
- As a simple statement, the unwrapped value must be of type
unit. This is analogous to an asynchronous operation with no direct result, or an option type indicating the presence of a value but not necessarily the value itself, and so on. - When the statement is followed by
!(if operators are necessary, the expression must be in parentheses), the result of the operation is explicitly discarded, even if it is non-unit. - As an expression, this pseudo-operator is usable only:
- As the immediate argument of
return,yield,yield return, orcontinue. - As the immediately assigned value in an assignment statement, variable declaration,
if letorwhile let. If multiple variables are declared in a single statement, all of their initialisation expressions must usefollow. - As an element in a collection construction with a builder.
- As the condition in
if,while, orrepeat…until, the tested value inswitch, and the collection infor…in. In most of these situations, the support is just syntactic sugar over assigning the value first to a variable and then using it.
- As the immediate argument of
Example
The most common usage of computation blocks is for asynchronous programming. The monadic bind operation invoked via follow is generally called "await" in such contexts, and represents scheduling the next piece of code to be executed only when another asynchronous operation completes:
function messages()
with async
echo "Start"
follow Async.Sleep(1000)
echo "After 1 second"
follow Async.Sleep(2000)
echo "After 2 seconds"
end
The async workflow supports controlling many aspects of the asynchronous operation, including lazy execution and cancellation. By default, objects created via async need to be explicitly executed:
let t = messages()
// No execution yet
Async.StartImmediate(t)
// "Start" shown
with
The with statement (not to be confused with the with operator or the with pattern) is used to transform the current scope into an isolated computation block. This causes the use of follow and yield (and possibly other custom operations usable as statements) to be translated into method calls on a builder object, retrieved by evaluating the expression after with.
Syntax
with_statement:
('follow' | 'yield')? 'with' '..'? (expression | '(' ')');
The argument of with affects the (mode)[#computation-modes] in which the statements after it operate ‒ if .. is used, the rest of the block is treated as a sequence of value, with no final value (return is disabled). If () is used, the default builder is used, switching to the immediate implementation of follow, and interpreting yield as creating simple sequences (type ..).
The permitted usage of this statement depends on its actual form (with, follow with, etc.):
- Without any other modifier prior to
with, the whole block starting from thewithstatement up until the end of the scope becomes the result of the outer block, as ifreturnwas called instead ofwith. This object corresponds to the operations performed in the block, or their result, in a manner defined by the workflow. Additionally, since there is no requirement that the code followingwithis executed at the point the result is created, it is not possible to have any additional statements following the whole block containing thewithstatement, unless the inner block is properly terminated. Awithblock also cannot be in atryblock (or an implicittrycreated byuse) because its effect cannot be observed from within the workflow. To bypass these restrictions and make returning explicit,return inline do withcan be used to open a new isolated block as the returned expression, or one of the other combining keywords can be used. - The
follow withstatement combines bothfollowandwithinto a statement that can serve as a shorthand forfollow inline do with, i.e. executes the rest of the block in the requested workflow, but continues the original code when it finishes. This has the benefit of exposing any side effects and flow control from inside of the computation block to the outside ‒ usingreturn,break, orcontinueaffects the enclosing blocks naturally. Use of this statement is possible only when the outer workflow supportsfollowon the wrapper type of the inner workflow, and safe only if it guarantees the code will be executed fully before execution moves to the remaining code. - The
yield withstatement is a combination ofyield..andwith, i.e. it executesyield..in the outer computation, taking the object produced by the inner computation. It has otherwise the same guarantees and requirements asfollow with.