type : Valid
Correct by construction
The Valid
type is a simple wrapper. The only way to wrap something in Valid
is to supply a value and a Validator
for values of that type to the validate
function. A Validator
ensures only valid values are wrapped and also ensures
that invalid values are handled. This runtime validation provides the
compile-time guarantee that any Valid
encountered in the wild contains a value
that is valid according to supplied Validator
.
For example, assuming a validator called Natural
that ensures integer values
are greater than zero, the types, Valid Natural Int
(Purescript) and
Valid<Natural, int>
(C#) can only be inhabited by a Valid
value containing
an integer greater than zero. The Natural
type parameter is proof that the
validation has occurred.
Implementations
Purescript implementation
Valid type
A Valid
type needs to wrap a value and retain type-level information about
which validation was used.
newtype Valid v a = MkValid a
To ensure a Valid
value can be created only as the result of a validation, the
MkValid
constructor will not be exported.
Validator class
A Validator
takes a value and returns some higher kinded type parameterized by
that value’s type.
class Validator v f a where validator :: Proxy v -> a -> f a
The phantom type parameter v
is essentially a type-level label for the
Validator
instance, since for each f,a
pair there is likely to be more than
one possible validation.
The HKT, f
, denotes the strategy a Validator
instance uses to differentiate
valid and invalid results.
Some possibilities for f
are:
- Maybe
validate
returnsSome (Valid v a)
orNothing
. Simple but provides no reason why validation failed.- Either error
validate
returnsLeft error
orRight (Valid v a)
. LikeMaybe
but provides a reason for failure.- Validation error
validate
returnsFailure (NonEmptyList error)
orSuccess (Valid v a)
. LikeEither
but accumulates errors rather than stopping at the first one.
Validate function
The validate
function takes a value, validates it using the selected
Validator
instance, and wraps a valid result in the Valid
type.
validate :: ∀ v f a. (Validator v f a, Functor f) => a -> f (Valid v a) validate a = MkValid <$> validator (Proxy :: Proxy v) a
It constrains f
to be a Functor
so the MkValid
constructor function can be
mapped over the result of validator
.
Complete module
Here it is as a complete module.
module GetTyped.Valid ( class Validator , validator , Valid , validate ) where import Prelude (<$>) import Type.Proxy (Proxy(..)) newtype Valid v a = MkValid a class Validator v f a where validator :: Proxy v -> a -> f a validate :: ∀ v f a. (Validator v f a, Functor f) => a -> f (Valid v a) validate a = MkValid <$> validator (Proxy :: Proxy v) a
Flow implementation
// @flow export class Valid<V, A> { value: A; constructor(a: A) { this.value = a; } } function mkValid<V, A>(a: A): Valid<V, A> { return new Valid(a); } export type Success<A> = {tag: "success", value: A}; export type Failure<E> = {tag: "failure", errors: [E]}; export type Validation<E, A> = Success<A> | Failure<E>; export function success<E, A>(a: A): Validation<E, A> { return {tag: "success", value: a}; } export function failure<E, A>(errs: [E]): Validation<E, A> { return {tag: "failure", errors: errs}; } export function validationErrors<E, A>(v: Validation<E, A>): [E] { return (v.tag === "failure" ? v.errors : []); } export function mapValidation<E, A, B>(f: (a: A) => B, v: Validation<E, A>) : Validation<E, B> { return (v.tag === "failure" ? {tag: "failure", errors: v.errors} : {tag: "success", value: f(v.value)}); } export function takeLeftValidation<E, A>(l: Validation<E, A>, r: Validation<E, A>) : Validation<E, A> { return (l.tag === "failure" || r.tag === "failure" ? failure(validationErrors(l).concat(validationErrors(r))) : l); } export interface Validator<A> { validator(a: A): Validation<Error, A>; } export function validate<A, V: Validator<A>>(v: V, a: A) : Validation<Error, Valid<V, A>> { return mapValidation(mkValid, v.validator(a)); } class Integer { validator(x: number): Validation<Error, number> { return x % 1 === 0 ? success(x) : failure([new Error("floating")]); } } class Natural { validator(x: number): Validation<Error, number> { return takeLeftValidation( new Integer().validator(x), x > 0 ? success(x) : failure([new Error("<= 0")])); } } class Negative { validator(x: number): Validation<Error, number> { return (x > 0 ? success(x) : failure([new Error("Not negative")])); } } function addNats(x: Valid<Natural, number>, y: Valid<Natural, number>): number { return x.value + y.value; }
Static guarantees
Statically guaranteed validity
With Valid
it is possible to ensure at compile-time that a function argument
is valid according to a specific Validator
.
For example:
data Ascii instance asciiValidator :: Validator Ascii Maybe String where validator _ name = if isOnlyAscii name then Just name else Nothing usernameHash :: Valid Ascii String -> Int
The type Valid AsciiUsername String
of the argument to usernameHash
constitutes compile-time proof that any value the function receives will be
valid according to the AsciiUsername
validator.
This is because the only way to provide a Valid AsciiUsername String
to
usernameHash
is by calling validate
on a String
.
This would look something like:
map usernameHash (validate usernameString) :: Maybe Int
Statically guaranteed invalid result handling
In a language with higher kinds, a Validator
can be written to use whichever
result handling makes most sense by using an appropriate type constructor for
f
. The type of f
decides what result handing is required.
A polymorphic f
also allows validation to be performed in an effectful
context, though that can reduce the strength of the guarantee depending on what
effects are allowed. E.g. If validity becomes time-dependent the compile-time
guarantee is no longer a complete one, but becomes, “this value is valid
according to v
validator modulo time-dependent factors”.