Absence and logic
In most languages a field either holds a value or holds nothing, and "nothing" is one thing. FormScript has four not-a-values, and the difference between them is the point of the language.
| The value | It means |
|---|---|
| unknown | the question has not been answered yet |
| inapplicable | the question is not being asked |
null |
the answer is deliberately empty |
| the error value | the expression could not be evaluated |
Unknown
A field that has never been answered holds unknown. Not zero, not the empty string — unknown.
form applicant version = "1.0" {
field name { type = string }
field quantity { type = integer }
field total { type = number value = 10 * quantity }
}
Before quantity is answered, total is unknown. It is not 0, because
"we do not know how many" and "there are none" are different facts, and a form
that cannot tell them apart will one day show a zero that was never a number.
Inapplicable
A field whose when condition is false is inapplicable. It is not asked,
it is not rendered, it is absent from the output document, and it does not
block submission. Section 10 covers
when in full.
null
null is an answer. It says the field was answered, and the answer is
deliberately empty — an optional field the person chose to leave blank, where
the form asked anyway.
The error value
An expression that cannot be evaluated produces the error value:
field a { type = number value = 1 / 0 }
field b { type = string value = "abc" + 1 }
The field is invalid. The document still runs.
Strict and absence-aware operations
Most operations are strict: if an operand is absent, the result is absent too, and the absence spreads.
field total { type = number value = price * quantity }
If quantity is unknown, total is unknown — not zero. * does not treat
unknown as zero.
Some builtins are absence-aware: they take an absence and give an answer instead.
| Builtin | What it does |
|---|---|
isEmpty(v) |
is the value absent or empty? |
isUnknown(v) |
is the value unknown? |
isInapplicable(v) |
is the value inapplicable? |
isAbsent(v) |
is the value any kind of absence? |
isKnown(v) |
is the value a real value? |
coalesce(a, b, …) |
the first argument that is neither absent nor empty |
ifNull(a, b) |
b if a is absent or empty, otherwise a |
orDefault(a, b) |
b if a is absent, otherwise a |
iif(cond, a, b) |
a if cond is true, b otherwise |
field name_or_blank { type = string value = coalesce(name, "") }
field greeting { type = string value = "Hello, " + coalesce(name, "there") }
coalesce is the workhorse: it takes the first argument that is neither
absent nor empty, so an empty string falls through to the next argument.
Logic is three-valued
and, or and ! do not return true and false. They return true,
false, or unknown.
a |
b |
a and b |
|---|---|---|
false |
anything | false |
true |
true |
true |
true |
unknown | unknown |
| unknown | unknown | unknown |
Read the first row carefully: false wins over everything. An and whose
result is false is false even when the other operand was an error. And an
error dominates an absence — but a false dominates an error.
The table for or is the mirror image: true wins over everything.
! gives the third value when its operand is the third value: !unknown is
unknown.
Why three values and not two
Because "the answer is not yet known" and "the answer is no" are different facts, and a form that treats them as the same produces forms that are wrong in a way nobody notices.
The clearest example is a when guard:
field guarantor_name {
type = string
when = loan_amount > 10000
}
If loan_amount is unknown, the comparison is unknown, and the field is
inapplicable — the question is not asked. That is not "asked and
answered no". If it were, a form that asks for a guarantor before the
applicant has typed the amount would demand a guarantor from everybody.
The ? : conditional
The conditional expression follows the same rule:
field band { type = string value = income > 100000 ? "high" : "standard" }
- condition
true→ the first branch; - condition
false→ the second branch; - condition unknown → unknown, not the second branch.
The conditional is strict in both branches: if the branch that is chosen is itself unknown, the result is unknown.
?? and absence
?? is the one operator that asks about absence directly:
field label { type = string value = name ?? "not given" }
If name is unknown, inapplicable, null or empty, the result is
"not given". Otherwise it is the value of name.
For a test that separates the kinds of absence, use isUnknown,
isInapplicable, isAbsent and isKnown — they are in
the builtin library.