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 within 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.
Computation blocks are operated using a combination of the with
statement and the follow
statement/pseudo-operator.
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 has the following effects:
- The expression following
with
is evaluated and used to retrieve a builder object that defines the computation type and its operations. - The
follow
operator becomes available (and possibly other custom operations usable as statements), translated into method calls on the builder. - The whole block starting from the
with
statement up until the end of the scope becomes the result of the block, as ifreturn
was called instead ofwith
. This object corresponds to the operations performed in the block, or their result, in a manner defined by the workflow. - Since there is no requirement that the code following
with
is executed at the point the result is created, it is not possible to have any additional statements following the whole block containing thewith
statement, unless that block is properly terminated. For this reason, a non-closedwith
block also cannot be in a loop or atry
block (or an implicittry
created byuse
). To bypass these restrictions and make returning explicit,return inline do with
can be used to open a new isolated block as the returned expression.
Syntax
with_statement:
'with' expression;
Examples
See the follow
keyword for examples.
follow
The follow
keyword is usable both as a single statement, and as a part of specific other statements, to "unwrap" its sole argument using the workflow previously entered via with
. The use of this keyword is translated into invoking the monadic "bind" operation on the builder object previously retrieved during with
, together with a function (formed from the rest of the block after follow
) that takes the "unwrapped" value as its argument, leaving it to the builder to decide how to bind the function to the argument of follow
, and when to execute it.
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).
Syntax
follow_statement:
'follow' expression;
follow_discard_statement:
'follow' member_expression '!';
follow_expression:
'follow' unary_expression;
Each usage 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 unary 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 let
orwhile let
. If multiple variables are declared in a single statement, all of their initialisation expressions must usefollow
. - 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