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:
input-a and input-b, for which we had to come up
with meaningful identifiers.
input-a and input-b
rather than a and b.
input-a and input-b
rather than a and b.
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.
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)
|
Node attributes do not form part of a runtime node’s state. |
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.
The |
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 -> bCompared 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.