Last active
July 24, 2019 14:08
-
-
Save reidev275/a99bfe785ec65ba4d23d51e3e84b99c3 to your computer and use it in GitHub Desktop.
Validation with Monoidal Contravariant Functors
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// a type to contain information about validation results | |
export type Validation = | |
| { kind: "Success" } | |
| { kind: "Failure"; errors: string[] }; | |
// helper functions to create Validation objects | |
const success: Validation = { kind: "Success" }; | |
const failure = (errors: string[]): Validation => ({ | |
kind: "Failure", | |
errors: errors | |
}); | |
type PredValidation<A> = (a: A) => Validation; | |
// proof we can combine 0-many PredValidation<A> types in such | |
// a way that all validations must pass to be successful | |
// otherwise aggregate all errors | |
const predValidationMonoid = <A>() => ({ | |
empty: () => success, | |
append: (x, y) => (a: A) => { | |
const v1 = x(a); | |
const v2 = y(a); | |
let errors = []; | |
if (v1.kind === "Failure") { | |
errors = [...errors, ...v1.errors]; | |
} | |
if (v2.kind === "Failure") { | |
errors = [...errors, ...v2.errors]; | |
} | |
if (v1.kind === "Success" && v2.kind === "Success") return v1; | |
return failure(errors); | |
} | |
}); | |
//primitive validation rules | |
const maxLength = (maxLength: number) => (s: string): Validation => | |
s.length < maxLength | |
? success | |
: failure([`Input exceeds ${maxLength} characters: ${s}`]); | |
const positive = (n: number): Validation => | |
n > 0 ? success : failure([`Input should be positive: ${n}`]); | |
//non primitive type | |
type Person = { | |
first: string; | |
last: string; | |
age: number; | |
}; | |
// a way to make primitive validation rules work for a non primitive type | |
const contramap = <A, B>( | |
f: (b: B) => A, | |
v: PredValidation<A> | |
): PredValidation<B> => (b: B) => v(f(b)); | |
// a way to combining multiple validation rules into a single validation rule | |
const combine = <A>(...as: PredValidation<A>[]): PredValidation<A> => { | |
const M = predValidationMonoid<A>(); | |
return as.reduce(M.append, M.empty); | |
}; | |
// a compound validation rule | |
const personValidation = combine( | |
contramap((x: Person) => x.age, positive), | |
contramap((x: Person) => x.first, maxLength(20)), | |
contramap((x: Person) => x.last, maxLength(25)) | |
); | |
// | |
// examples | |
const valid = personValidation({ | |
first: "Reid", | |
last: "Evans", | |
age: 2 | |
}); | |
// { kind: "Success" } | |
const invalid = personValidation({ | |
first: "Reid", | |
last: "Evans", | |
age: -2 | |
}); | |
// { kind: "Failure", errors: [ "Input should be positive: -2" ] } | |
const invalidTwice = personValidation({ | |
first: "this is a really long name", | |
last: "Evans", | |
age: -2 | |
}); | |
// { kind: "Failure", errors: [ "Input should be positive: -2", "Input exceeds 20 characters: this is a really long name" ] } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment