8. Target Node Transforms

Wow, we had to make so many fundamental changes to our code just to implement a minor change in the input accepted by the application. We had to:

  1. Add the nodes input-a and input-b, for which we had to come up with meaningful identifiers.
  2. Change the input fields to be bound to input-a and input-b rather than a and b.
  3. Change the initial values to be assigned to input-a and input-b rather than a and b.
  4. Bind a to validate(input-a) and b to validate(input-b).

This is contrary to “simply adding new UI elements” which was the case when we introduced error handling. We can do better.

Notice that a lot of the code we added was simply repetitive binding boilerplate code, which is the same for both a and b. It would be nice if we could somehow abstract it away and not have to write the same code for both nodes. Luckily, there is a way.

Remember, from the second tutorial, that some meta-nodes, such as to-int, are special in that a two-way binding is established between the meta-node instance and the argument node. This allows instances of the meta-node to also appear as targets of bindings.

Refresher Example. 

# The following
a -> to-int(b)

# Is equivalent to
to-int(a) -> b

It turns out to-int is not so special as we can do the same for our own meta-nodes by setting the target-node attribute.

8.1. Node Attributes

Node attributes are simply key-value pairs associated with a node, which control various compilation options. Attributes are set using the special /attribute operator:

/attribute Operator Syntax. 

/attribute(node, key, value)

This sets the attribute of node with key key to the value value.

Examples. 

# Set value of attribute `my-attribute` to 1
/attribute(a, my-attribute, 1)

# Set value of attribute `akey` to literal symbol `raw-id`
/attribute(b, "akey", raw-id)

[Important]

key and value are interpreted as literal symbols rather than references to the values of nodes. Attribute keys are case insensitive and there is no difference between raw symbols and string keys. The following keys key, Key, "key" and "kEy" all refer to the same attribute.

[Important]

Node attributes do not form part of a runtime node’s state.

8.2. Attribute target-node

The target-node attribute determines, when set, the meta-node which is used as the binding function of the binding in the reverse direction, from a meta-node instance to the meta-node arguments.

As an example, a meta-node f with its target-node attribute set to g results in the following:

Example. 

/attribute(f, target-node, g)

# The following
a -> f(b)

# Is equivalent to
g(a) -> b

In the example above the target-node attribute of f is set to g. Thus the declaration f(b) also results in the binding g(f(b)) -> b being created.

The meta-node to-int simply has its target-node attribute set to itself, which is why it performs the same function, when it appears as the target of a binding, as when it appears as the source of a binding.

[Tip]

The to-int meta-node performs the same function as the int meta-node however the difference is that when an instance of int appears as the target of a binding, pattern matching (which will be introduced in a later tutorial) is performed, whereas to-int simply performs the same function. int has not been mentioned till this point to avoid creating confusion as to what’s the difference between it and to-int.

8.3. Target-Node for own Meta-Nodes

Our code can be simplified considerably by allowing a meta-node, which performs the additional input validation, to be bound (as the target) to the values in the input field. Let’s first write that meta-node, called valid-int which is responsible for converting an input string to an integer and ensuring that the resulting integer is greater than or equal to zero. In essence this meta-node combines to-int, we’ll use int this time, and validate.

Meta-node valid-int

valid-int(value) : {
    x <- int(value)
    validate(x)
}

In order to allow the node to appear as the target of a binding, and still perform the same function, let’s set its target-node attribute to itself:

/attribute(valid-int, target-node, valid-int)

Now we can bind the contents of the input fields directly to an instance of the valid-int meta-node. In-fact, we can place the valid-int instance directly in an inline node declaration.

Replace to-int(input-a) with valid-int(a), and the same for b, in the input fields as follows:

<label>A: <input value="<?@ valid-int(a) ?>"/></label>
<label>B: <input value="<?@ valid-int(b) ?>"/></label>

The nodes input-a and input-b can be removed, as well as the following declarations:

 a <- validate(input-a)
 b <- validate(input-b)

The initial values of 0 can once again be given to the nodes a and b rather than input-a and input-b.

 0 -> a
 0 -> b

The following is the full content of the Tridash code tag.

/import(core)

# Error Reporting

error-message(value) :
    case(
        fail-type?(value, Invalid-Integer) :
            "Not a valid number!",
        fail-type?(value, Negative-Number) :
            "Number must be greater than or equal to 0!",
        ""
    )

# Input Validation

Negative-Number  <- &(Negative-Number)
Negative-Number! <- fail(Negative-Number)

validate(x) :
    case(
        x >= 0 : x,
        Negative-Number!
    )

valid-int(value) : {
    x <- int(value)
    validate(x)
}

/attribute(valid-int, target-node, valid-int)

# Initial Values

0 -> a
0 -> b

Compared to the previous version, the only modifications are in the error-message meta-node, the inline bindings in the input fields and the addition of the validate and valid-int meta-nodes along with the Negative-Number failure type. This version, however, did not require the addition of new nodes or modifying the bindings comprising the core application logic. Changing the input validation logic was simply a matter of substituting to-int with valid-int in the bindings to the input field values.