Skip to content

Commit

Permalink
improve documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
veigaribo committed Oct 9, 2020
1 parent b1b4731 commit 5314098
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 12 deletions.
30 changes: 18 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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({
Expand All @@ -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:

Expand All @@ -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)
Expand All @@ -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.

Expand Down
23 changes: 23 additions & 0 deletions src/classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,34 @@ import { all, obey, ValidationResult, Validator } from './validators'
type Laws = Validator<Instance>

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
Expand Down
5 changes: 5 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const config = {
[skipValidations]: false,
[testSampleSize]: 15,

/** A callable that shall return a random number. */
get generateRandom() {
return this[generateRandom]
},
Expand All @@ -15,6 +16,7 @@ export const config = {
this[generateRandom] = value
},

/** If true, class validations will not be performed. */
get skipValidations() {
return this[skipValidations]
},
Expand All @@ -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]
},
Expand Down
25 changes: 25 additions & 0 deletions src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends InstanceConstructor>(
theClass: Class,
): (constructor: T) => T {
Expand Down
6 changes: 6 additions & 0 deletions src/instances.ts
Original file line number Diff line number Diff line change
@@ -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<this>
Expand Down
37 changes: 37 additions & 0 deletions src/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends InstanceConstructor>(
predicate: Predicate<InstanceType<T>>,
) {
return new Obeys<T>(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)
}

0 comments on commit 5314098

Please sign in to comment.