diff --git a/README.md b/README.md index e58bf43..0383f51 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,19 @@ # Lawful Type Classes `lawful-typeclasses` is a library designed to provide a way of asserting -the behavior of your JS classes. +the behavior of your JavaScript classes. "Lawful" here refers to a characteristic of [principled type classes](https://degoes.net/articles/principled-typeclasses). ## What it does This library allows you to define two things: classes and instances. Perhaps -a bit confusedly, classes are JS objects and instances are JS classes. +a bit confusedly, classes are JavaScript objects and instances are +JavaScript classes. + +We'll be referring to the JavaScript classes that implement the behavior of +a type class (and are thus _instances_ of that class) as _constructors_ and +to the instances of those JavaScript classes as _instance values_. ### Classes @@ -27,7 +32,8 @@ const addable = new Class({ name: 'Addable', // next, we define the properties we expect our instances to have // we'll start out by using the `all` function to say that, in order to - // be an Addable, the class must obey all of the following laws (not any) + // be an Addable, the constructor must obey all of the following laws + // (not any) laws: all( obey((x, y) => { // we expect addition to be commutative @@ -65,7 +71,7 @@ const eq = new Class({ ``` And then the Addable class may _extend_ Eq, meaning that, in order to be an -instance of Addable, the JS class must also be an instance of Eq: +instance of Addable, the constructor must also be an instance of Eq: ```javascript const addable = new Class({ @@ -77,7 +83,7 @@ const addable = new Class({ ### Instances -Instances are JS classes that behave according to some (type) class. +Instances are JavaScript classes that behave according to some (type) class. Using the Addable example above, one could almost define an instance as: @@ -100,7 +106,7 @@ class Number { The only extra step is to define a static method called `generateData`, that will take any number of random numbers in the range [0, 1] as parameters -and should return a random JS instance of the JS class. +and should return a random instance value of the constructor. ```javascript @instance(addable) @@ -116,12 +122,12 @@ class Number { ## How it works -When you define your JS class using the `@instance` decorator, a sample of -random JS instances of your JS class will be generated using your JS classes' -`generateData`, and each property will be tested using those. If any of the -laws fails to be asserted, an error is thrown, and you may be sure that the -JS class in question is not an instance of the class you declared in the -decorator. +When you define your constructor using the `@instance` decorator, a sample +of random instance values of your constructor will be generated using your +constructor's `generateData`, and each property will be tested using those. +If any of the laws fails to be asserted, an error is thrown, and you may be +sure that the constructor in question is not an instance of the class you +declared in the decorator. In case it passes, you may have a high confidence that it is. diff --git a/src/classes.ts b/src/classes.ts index a45015d..affbb2e 100644 --- a/src/classes.ts +++ b/src/classes.ts @@ -5,11 +5,34 @@ import { all, obey, ValidationResult, Validator } from './validators' type Laws = Validator export interface ClassOptions { + /** The name will be used to improve error messages. */ name?: string + /** A list of classes that are prerequisites to this one. */ extends?: Class[] + /** A validator that will check if a constructor is an instance of this class. */ laws?: Laws } +/** + * A class defines the behavior that your instances shall have. + * + * The behavior will be asserted using the given laws (if a given constructor is declared + * to be an instance of a given class, but it does not pass its validations, an error is + * thrown). + * + * A class may also extend other classes, so all their validators must pass as well. + * + * @example + * ```javascript + * const monoid = new Class({ + * name: 'Monoid', + * extends: [eq], + * laws: all(append, commutativity, associativity, identity) + * }) + * ``` + * + * @see {@link all} + */ export class Class { public readonly parents: Class[] public readonly laws: Laws diff --git a/src/config.ts b/src/config.ts index 50e8022..6a1e567 100644 --- a/src/config.ts +++ b/src/config.ts @@ -7,6 +7,7 @@ export const config = { [skipValidations]: false, [testSampleSize]: 15, + /** A callable that shall return a random number. */ get generateRandom() { return this[generateRandom] }, @@ -15,6 +16,7 @@ export const config = { this[generateRandom] = value }, + /** If true, class validations will not be performed. */ get skipValidations() { return this[skipValidations] }, @@ -23,6 +25,9 @@ export const config = { this[skipValidations] = !!value }, + /** The number of times each instance will be validated against its supposed class. + * Note that, because of the edge cases 0 and 1, that are always tested against, this + * effectively has a minimum value of 2. */ get testSampleSize() { return this[testSampleSize] }, diff --git a/src/decorators.ts b/src/decorators.ts index f2080f9..0437e9f 100644 --- a/src/decorators.ts +++ b/src/decorators.ts @@ -2,6 +2,31 @@ import { Class } from './classes' import { InstanceConstructor } from './instances' import { Right } from './utils' +/** + * Declares a constructor to be an instance of the given class. + * If the validation fails, an error is thrown. + * + * @param theClass - The class that this is an instance of. + * @returns A function that checks whether the given constructor is an instance of theClass + * or not. + * @throws If the class validations fail. + * + * @example + * ```javascript + * // this should not be a string, JSDoc's fault + * `@instance(semigroup)` + * class Addition { + * constructor(x) { this.x = x } + * add(y) { return new Addition(this.x + y.x) } + * } + * ``` + * + * @example + * ``` + * // if you cannot use decorators, just call it as a function + * instance(semigroup)(Addition) + * ``` + */ export function instance( theClass: Class, ): (constructor: T) => T { diff --git a/src/instances.ts b/src/instances.ts index d1009eb..5cbdc40 100644 --- a/src/instances.ts +++ b/src/instances.ts @@ -1,3 +1,9 @@ +/** + * An InstanceConstructor must implement a `generateData` method, that shall + * generate random instance values based on any amount of random numbers. + * + * Those random instances will be used to check against the class laws. + */ export interface InstanceConstructor extends Function { // @ts-ignore new (...args: any[]): InstanceType diff --git a/src/validators.ts b/src/validators.ts index f35d86b..7a6a1cb 100644 --- a/src/validators.ts +++ b/src/validators.ts @@ -117,16 +117,53 @@ export class Any implements InstanceValidator { } } +/** + * Defines a behavior that the instance values must follow. + * + * @param predicate - The function that, given any number of random instance values, shall return true. + * + * @example + * ```javascript + * obey(function commutativity(a, b) { + * // this property shall hold for any values a and b + * return a.add(b) === b.add(a) + * }) + * ``` + */ export function obey( predicate: Predicate>, ) { return new Obeys(predicate) } +/** + * Compose multiple validators using a logical AND. + * + * @param laws - The individual validators that must be followed. + * + * @example + * ```javascript + * // assuming associativity, commutativity and identity are validators, this will return + * // another validator that demands every one of those laws to be obeyed + * all(associativity, commutativity, identity) + * ``` + */ export function all(...laws: InstanceValidator[]): InstanceValidator { return new All(laws) } +/** + * Compose multiple validators using a logical OR. + * + * @param laws - The individual validators, where at least one must be followed. + * + * @example + * ``` + * // assuming symmetry and antissymetry are validators, this will return another validator + * // that demands at least one of those laws to be obeyed + * any(symmetry, antisymmetry) + * ``` + */ export function any(...laws: InstanceValidator[]): InstanceValidator { return new Any(laws) }