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 if return was called instead of with. 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 the with statement, unless that block is properly terminated. For this reason, a non-closed with block also cannot be in a loop or a try block (or an implicit try created by use). 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, or continue.
    • As the immediately assigned value in an assignment statement, variable declaration, if let or while let. If multiple variables are declared in a single statement, all of their initialisation expressions must use follow.
    • As the condition in if, while, or repeat…until, the tested value in switch, and the collection in for…in. In most of these situations, the support is just syntactic sugar over assigning the value first to a variable and then using it.

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