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
validatereturnsSome (Valid v a)orNothing. Simple but provides no reason why validation failed.- Either error
validatereturnsLeft errororRight (Valid v a). LikeMaybebut provides a reason for failure.- Validation error
validatereturnsFailure (NonEmptyList error)orSuccess (Valid v a). LikeEitherbut 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”.