9. Contexts

Throughout these tutorials, we’ve glossed over two-way bindings without going into much detail of how they work, yet they were a vital component of every application as the bindings to the UI elements have all been two-way bindings.

Each node has a number of contexts which store information about how to compute the node’s value, i.e. what function to use and what dependencies are operands to the function. The active context of a node, at a given moment in time, is the context which is used to compute the node’s value. In general, a context is activated when the value of an operand node of the context changes. By default, a node context is created for each dependency of a node which was added by an explicit binding.

Example. 

a -> x # Context created for dependency `a`
b -> x # Context created for dependency `b`
c -> x # Context created for dependency `c`

In this example node x has three contexts one for each of its dependency nodes, a, b and c, to which it is bound explicitly.

An implicit binding between a meta-node instance and the meta-node arguments does not result in the creation of a context for each operand.

a + b

Nodes a and b are implicitly added as dependencies of a + b however they are added as operands to the same context with the + function.

The following application demonstrates how different contexts are activated, when the values of their operand nodes change.

ui.html

<?
 x -> node
 y -> node
 z -> node
?>
<!doctype html>
<html>
    <head>
        <title>Node Contexts</title>
    </head>
    <body>
      <div><label>X: <input value="<?@ x ?>"/></label></div>
      <div><label>Y: <input id="b" value="<?@ y ?>"/></label></div>
      <div><label>Z: <input value="<?@ z ?>"/></label></div>
      <hr/>
      <div><strong>Last value entered: <?@ node ?></strong></div>
    </body>
</html>

This is a simple application consisting of three text input fields bound to nodes x, y and z. Nodes x, y and z are each explicitly bound to node, the value of which is displayed below the fields.

Let’s enter a value in each field and see what happens. Observe the value displayed below the fields after each change:

X: 1, Y: _, Z: _, Last value entered: 1
X: 1, Y: 2, Z: _, Last value entered: 2
X: 1, Y: 2, Z: 3, Last value entered: 3

Notice that after each change, the value that was just entered is displayed.

Now let’s try changing the values of the fields which were edited previously:

X: 1, Y: 10, Z: 3, Last value entered: 10

In this case the value of the second field, Y, was changed to 10 and that value was immediately displayed below the fields.

The value of the field that was changed last is displayed. To understand why this is so, let’s examine the sequence of steps taken when a value is entered in the X field.

  1. The value of x, which is bound to the value in the X field, is updated.
  2. The context corresponding to the binding x -> node is activated due to the value of x being updated.
  3. The value of node is updated to the value of x.

Contexts make two-way bindings possible:

Example. 

input1 -> a

# Two-way binding
a -> b; a -> b

input2 -> b

[Tip]

The ; character separates multiple declarations written on a single line.

a has two contexts corresponding to dependency nodes input1 and b (which is also an observer). b has two contexts corresponding to dependency nodes input2 and a.

When input1 is changed, the contexts corresponding to the bindings in the following direction are activated:

When input2 is changed, the contexts corresponding to the bindings in the following direction are activated:

9.1. Explicit Contexts

The context of a binding can be set explicitly to a named context, using the @ operator from the core module.

a -> b @ context-id

The binding a -> b is established in the context, of b, with identifier context-id.

When multiple bindings are established to the same explicit context, the observer node takes on the value of the first operand which does not evaluate to a failure. The operands are ordered by the order in which the explicit bindings are declared in the source file. If all the operands evaluate to failures, the node evaluates to the failure value of the last operand.

This is better explained with an example application:

ui.html

<?
 /import(core)

 x -> node @ context
 y -> node @ context
 z -> node @ context
?>
<!doctype html>
<html>
    <head>
        <title>Explicit Contexts</title>
    </head>
    <body>
      <div><label>X: <input value="<?@ to-int(x) ?>"/></label></div>
      <div><label>Y: <input value="<?@ to-int(y) ?>"/></label></div>
      <div><label>Z: <input value="<?@ to-int(z) ?>"/></label></div>
      <hr/>
      <div><strong>Value: <?@ node ?></strong></div>
    </body>
</html>

This application is similar to the previous application except the bindings from nodes x, y and z, to node are established in an explicit context with identifier context. Additionally the input fields are bound to to-int instances, of x, y and z which results in x, y and z being bound to the values entered in the fields converted to integers. If a non-integer value is entered in a field, the corresponding node is bound to a failure value.

Let’s try it out. Enter some integer values in each of the fields:

X: 1, Y: 2, Z: 3, Value: 1

The value entered in the first field, X, was displayed. Since a valid integer was entered, node x evaluates to the integer value 1. The binding x -> node was established first, as the declaration occurs first in the source file, and since x does not evaluate to a failure, node takes on the value of x. The values of y and z are ignored.

Now let’s change x to a non-integer value:

X: foo, Y: 2, Z: 3, Value: 2

The value entered in the second field, 2, is displayed. Since a non-integer value was entered in the first field, x evaluates to a failure. node thus takes on the value of the next dependency, bound to the explicit context, which does not evaluate to a failure. The dependency is y which evaluates to the integer entered in the second field, 2.

Let’s see what happens if we enter a non-integer value in the third field:

X: foo, Y: 2, Z: bar, Value: 2

The displayed value is unchanged since the second dependency, node y, already evaluates to a value which is not a failure value. The value of the third dependency z, corresponding to the value entered in the third field, is ignored, regardless of whether it evaluates to a failure or not.

9.2. Handling Failures with Explicit Contexts

Explicit contexts are a useful tool for handling failures. In the previous application a failure originating from the first input field, was handled by taking the value of the node bound to the second field. Similarly a failure originating from the second input field is handled by taking the value entered in the third field.

The @ operator also allows a binding to be activated only if the result of the previous binding(s), in the same context, is a failure value with a given type. When the context identifier is of the form when(context, type) the binding is only activated if the result of the previous binding(s) is a failure of type type.

Example. 

x -> node @ context
y -> node @ when(context, Invalid-Integer)
z -> node @ when(context, Negative-Number)

Three bindings to node are established in the explicit context context.

node is primarily bound to the value of x if it does not evaluate to a failure. If x evaluates to a failure of type Invalid-Integer, node is bound to the value of y. If x, or y evaluate to a failure of type Negative-Number, then node is bound to the value of z.

To try this out replace the binding declarations, in the application from the previous section, with the declarations in the example above. Also copy over the definition of the meta-nodes valid-int, validate and the Negative-Number failure type from Section 8.3, “Target-Node for own Meta-Nodes”, into the Tridash code tag. Replace to-int with valid-int in the inline node declarations within the input field values.

Enter a non-integer value in the first field, and an integer value in the second and third fields:

X: foo, Y: 1, Z: 2, Value: 1

The value of the second field is displayed, since node is bound to it when the value in the first field is not an integer.

Now change the value of the second field to a negative integer, or alternatively enter a negative integer value in the first field:

X: foo, Y: -1, Z: 2, Value: 2
X: -1, Y: 1, Z: 2, Value: 2

The value of the third field is displayed in both cases, even when the value of the second field is a valid positive integer. This is due to node being bound to the value of the third field when either the value of the first field or second field is a negative number.

[Tip]

when is registered as an infix operator thus the following:

a -> b @ when(context, type)

can be rewritten as:

a -> b @ context when type

9.3. Improved Error Handling in Adding Numbers

Whilst the error handling logic in the Adding Numbers application, from Section 8, “Target Node Transforms”, is adequate and correct, the definition of the error-message meta-node, responsible for selecting an appropriate error message, can be improved using explicit contexts. The current definition repeatedly checks whether the failure type of the value argument is of a given type using the fail-type? meta-node. This is repetitive and does not convey the intent that this is error handling/reporting logic.

The error-message meta-node returns:

  • The empty string if the value argument does not evaluate to a failure.
  • The string “Not a valid number!” when value evaluates to a failure of type Invalid-Integer.
  • The string “Number must be greater than or equal to 0!” when value evaluates to a failure of type Negative-Number.

We can re-implement this logic using bindings to the self node with explicit contexts.

The self node should primarily be bound to the empty string, if the value argument does not evaluate to a failure. There is a handy utility meta-node, !-, in the core module, which returns the value of its second argument if the first argument does not evaluate to a failure. If the first argument evaluates to a failure value, it is returned. This meta-node is registered as an infix operator thus can be placed between its arguments.

The primary binding can thus be written as follows:

value !- "" ->
    self @ context

If this binding results in a failure of type Invalid-Integer, self should be bound to the constant string “Not a valid number!”. This is achieved with the following:

"Not a valid number!" ->
    self @ context when Invalid-Integer

Finally self should be bound to “Number must be greater than or equal to 0!”, if the previous bindings resulted in a failure of type Negative-Number.

"Number must be greater than or equal to 0!" ->
    self @ context when Negative-Number

Putting it all together we have the following definition of error-message re-implemented using explicit contexts:

New implementation of error-message

error-message(value) : {
    value !- "" ->
        self @ context

    "Not a valid number!" ->
        self @ context when Invalid-Integer

    "Number must be greater than or equal to 0!" ->
        self @ context when Negative-Number
}

The advantage of this implementation is that it more explicitly conveys the intent that this is error handling logic. As such it can be optimized more effectively, e.g. if self evaluates to a failure of type Negative-Number, the check for whether the failure type is Invalid-Integer can be skipped altogether.

An additional advantage of this implementation is that the third binding is activated on failures of type Negative-Number originating both from the first and second bindings whereas the previous implementation only handled failures originating in value. In this case it doesn’t make a difference as the second binding cannot result in a failure of type Negative-Number. However this does make a difference, in more complex error handling logic, where the handling of an error may itself result in a new error.

This implementation does, however, have a difference from the previous implementation in that if value evaluates to a failure of a type other than Invalid-Integer or Negative-Number it returns a failure, whereas the previous implementation returned the empty string. In this application it doesn’t make a difference as the arguments passed to error-message do not evaluate to failures of other types.

9.4. Concise Syntax

Coming up with a context identifier and typing it out repeatedly can become tiresome. The original reason for having an identifier for explicit contexts is to distinguish them from the remaining contexts which are implicitly created and to allow for multiple explicit contexts. However, there is usually only a single explicit context used for handling failures.

To shorten the syntax for binding to an explicit context, a default identifier, such as _ can be given to all explicit contexts which are used only for handling failures. Alternatively, the @ operator can take a single argument, the node, in which case it is a shorthand for the explicit context with identifier default.

# The following:
x -> y @ default

# Is equivalent to:
x -> @(y)

When the context identifier is of the form when(type), that is omitting the context identifier and leaving only the failure type, the explicit context with identifier default is, once again, assumed.

# The following:
x -> y @ default when type

# Is equivalent to:
x -> y @ when(type)