FS FormScript

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.

forms
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:

forms-fragment
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.

forms-fragment
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
forms-fragment
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:

forms-fragment
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:

forms-fragment
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:

forms-fragment
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.