validations

This library provides an implementation of validation terms with an API for applicative-style error accumulation. A validation term is an opaque compound term that either contains a valid value (valid(Value)) or a list of errors (invalid(Errors)).

The library follows the same design pattern as the optionals and expecteds libraries:

  • validation — factory object for constructing validation terms

  • validation/1 — parametric object wrapping a validation term

  • validated — companion helper object for list operations, type-checking, and QuickCheck support

The naming follows the convention used in Scala Cats and Kotlin Arrow, where the companion type is called Validated.

API documentation

Open the ../../apis/library_index.html#validations link in a web browser.

Loading

To load all entities in this library, load the loader.lgt file:

| ?- logtalk_load(validations(loader)).

Testing

To test this library predicates, load the tester.lgt file:

| ?- logtalk_load(validations(tester)).

Usage

The validation object provides constructors for validation terms. For example:

| ?- validation::of_valid(1, Validation).
...

The created validation terms can then be passed as parameters to the validation/1 parametric object. For example:

| ?- validation::of_valid(1, V1), validation::of_valid(2, V2),
     validation::sequence([V1, V2], Validation).
Validation = valid([1,2])
yes

| ?- validation::of_invalid(e1, V1), validation::of_invalid(e2, V2),
     validation::sequence([V1, V2], Validation).
Validation = invalid([e1,e2])
yes

| ?- validation(valid(1))::flat_map([X,V]>>(Y is X+1, V = valid(Y)), Validation).
Validation = valid(2)
yes

Validation terms can be constructed from goals, optionals, and expecteds:

| ?- validation::from_goal(succ(1, Value), Value, Validation).
Validation = valid(2)
yes

| ?- validation::from_optional(empty, no_value, Validation).
Validation = invalid([no_value])
yes

| ?- validation::from_expected(unexpected(e), Validation).
Validation = invalid([e])
yes

Validation terms can be converted to optionals and expecteds:

| ?- validation(valid(42))::to_optional(Optional).
Optional = optional(42)
yes

| ?- validation(invalid([e1,e2]))::to_expected(Expected).
Expected = unexpected([e1,e2])
yes

The key feature of validation is error accumulation via zip/3:

| ?- validation::of_valid(1, V1), validation::of_invalid(e1, V2),
     validation(V1)::zip([_,_,_]>>true, V2, Result).
Result = invalid([e1])
yes

| ?- validation::of_invalid(e1, V1), validation::of_invalid(e2, V2),
     validation(V1)::zip([_,_,_]>>true, V2, Result).
Result = invalid([e1,e2])
yes

The companion validated object provides predicates for handling lists of validation terms, including valids/2, invalids/2, partition/3, and map/3-4.

| ?- validation::of_valid(1, V1), validation::of_invalids([e1,e2], V2), validation::of_valid(2, V3),
     validated::partition([V1, V2, V3], Values, Errors).
Values = [1,2],
Errors = [e1,e2]
yes

| ?- validated::map([Term,Validation]>>(
       integer(Term) -> validation::of_valid(Term, Validation)
     ; validation::of_invalid(not_integer(Term), Validation)
     ), [1,a,2,b], ValuesErrors).
ValuesErrors = [1,2]-[not_integer(a),not_integer(b)]
yes

| ?- validated::map([Term,Validation]>>(
       integer(Term) -> validation::of_valid(Term, Validation)
     ; validation::of_invalid(not_integer(Term), Validation)
     ), [1,a,2,b], Values, Errors).
Values = [1,2],
Errors = [not_integer(a),not_integer(b)]
yes

Comparison with the expecteds library

Both this library and the expecteds library wrap computations that may succeed or fail. The fundamental difference is the error model:

  • expecteds carries a single error and short-circuits on the first failure (monadic error handling). Once an unexpected term is produced, subsequent operations are skipped.

  • validations carries a list of errors and accumulates all failures (applicative error handling). Operations such as zip/3, sequence/2, and traverse/3 collect every error.

Use expecteds when you only care about the first error. Use validations when you want to report all problems at once (e.g. form validation, configuration checking, or batch input processing).

Example: form validation with error accumulation

Consider validating a form with a name (must be an atom) and an age (must be a positive integer). Each field is validated independently and the results are combined with validated::sequence/2, which accumulates all errors:

| ?- Name = 123, Age = young,
     validation::from_goal(atom(Name), Name, invalid_name, V1),
     validation::from_goal((integer(Age), Age > 0), Age, invalid_age, V2),
     validated::sequence([V1, V2], Result),
     validation(Result)::or_else_throw(_).
uncaught exception: [invalid_name,invalid_age]

Both errors are reported. When all fields are valid, or_else_throw/1 returns the list of valid values:

| ?- Name = john, Age = 25,
     validation::from_goal(atom(Name), Name, invalid_name, V1),
     validation::from_goal((integer(Age), Age > 0), Age, invalid_age, V2),
     validated::sequence([V1, V2], Result),
     validation(Result)::or_else_throw(Values).
Values = [john,25]
yes

Compare with the expecteds library, where either::sequence/2 short-circuits at the first failure:

| ?- Name = 123, Age = young,
     expected::from_goal(atom(Name), Name, invalid_name, E1),
     expected::from_goal((integer(Age), Age > 0), Age, invalid_age, E2),
     either::sequence([E1, E2], Result),
     expected(Result)::or_else_throw(_).
uncaught exception: invalid_name

Only invalid_name is reported — either::sequence/2 stops at the first unexpected term.

The two libraries are complementary. Use expecteds for sequential pipelines where only the first error matters. Use validations with sequence/2, traverse/3, or zip/3 for independent validations where all errors should be collected.

See also

The expecteds and optionals libraries.