Skip to content

Commit

Permalink
feat(defmulti): add generics, update docs & readme
Browse files Browse the repository at this point in the history
  • Loading branch information
postspectacular committed May 11, 2018
1 parent 011519d commit eeed25e
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 19 deletions.
44 changes: 28 additions & 16 deletions packages/defmulti/README.md
Expand Up @@ -9,7 +9,9 @@ This project is part of the

Dynamically extensible [multiple
dispatch](https://en.wikipedia.org/wiki/Multiple_dispatch) via user
supplied dispatch function, with minimal overhead.
supplied dispatch function, with minimal overhead. Provides generics for
type checking up to 8 args, but generally works with any number of
arguments.

## Installation

Expand All @@ -22,11 +24,17 @@ yarn add @thi.ng/defmulti
### defmulti

`defmulti` returns a new multi-dispatch function using the provided
dispatcher function. The dispatcher can take any number of arguments and
must produce a dispatch value (string, number or symbol) used to lookup
an implementation. If found, the impl is called with the same args. If
no matching implementation is available, attempts to dispatch to
`DEFAULT` impl. If none is registered, an error is thrown.
dispatcher function. The dispatcher acts as a mapping function, can take
any number of arguments and must produce a dispatch value (string,
number or symbol) used to lookup an implementation. If found, the impl
is called with the same args. If no matching implementation is
available, attempts to dispatch to `DEFAULT` impl. If none is
registered, an error is thrown.

`defmulti` provides generics for type checking up to 8 args (plus the
return type) and the generics will also apply to all implementations. If
more than 8 args are required, `defmulti` will fall back to an untyped
varargs solution.

Implementations for different dispatch values can be added and removed
dynamically by calling `.add(id, fn)` or `.remove(id)` on the returned
Expand All @@ -35,7 +43,7 @@ function.
```typescript
import { defmulti, DEFAULT } from "@thi.ng/defmulti";

const visit = defmulti((x) => Object.prototype.toString.call(x));
const visit = defmulti<any, void>((x) => Object.prototype.toString.call(x));

// register implementations for different dispatch types
// each dispatch value can only be registered once
Expand All @@ -62,7 +70,7 @@ for a variation of this example.
#### Dynamic dispatch: Simple S-expression interpreter

```ts
const exec = defmulti((x)=> Array.isArray(x) ? x[0] : typeof x);
const exec = defmulti((x) => Array.isArray(x) ? x[0] : typeof x);
exec.add("+", ([_, ...args]) => args.reduce((acc, n) => acc + exec(n), 0));
exec.add("*", ([_, ...args]) => args.reduce((acc, n) => acc * exec(n), 1));
exec.add("number", (x) => x);
Expand All @@ -78,15 +86,16 @@ exec(["+", ["*", 10, ["+", 1, 2, 3]], 6]);
```ts
// interest rate calculator based on account type & balance thresholds
const apr = defmulti(
({type, balance}) => `${type}-${balance < 1e4 ? "low" : balance < 5e4 ? "med" : "high"}`
({type, balance}) =>
`${type}-${balance < 1e4 ? "low" : balance < 5e4 ? "med" : "high"}`
);

apr.add("current-low", ({balance}) => balance * 0.005);
apr.add("current-med", ({balance}) => balance * 0.01);
apr.add("current-high", ({balance}) => balance * 0.01);
apr.add("savings-low", ({balance}) => balance * 0.01);
apr.add("savings-med", ({balance}) => balance * 0.025);
apr.add("savings-high", ({balance}) => balance * 0.035);
apr.add("current-low", ({ balance }) => balance * 0.005);
apr.add("current-med", ({ balance }) => balance * 0.01);
apr.add("current-high", ({ balance }) => balance * 0.01);
apr.add("savings-low", ({ balance }) => balance * 0.01);
apr.add("savings-med", ({ balance }) => balance * 0.025);
apr.add("savings-high", ({ balance }) => balance * 0.035);
apr.add(DEFAULT, (x) => { throw new Error(`invalid account type: ${x.type}`)});

apr({type: "current", balance: 5000});
Expand All @@ -108,8 +117,11 @@ added (or removed) at a later time. `defmultiN` also registers a
`DEFAULT` implementation which simply throws an `IllegalArityError` when
invoked.

**Note:** Unlike `defmulti` no argument type checking is supported,
however you can specify the return type for the generated function.

```ts
const foo = defmultiN({
const foo = defmultiN<string>({
0: () => "zero",
1: (x) => `one: ${x}`,
3: (x, y, z) => `three: ${x}, ${y}, ${z}`
Expand Down
84 changes: 81 additions & 3 deletions packages/defmulti/src/index.ts
Expand Up @@ -5,13 +5,70 @@ import { illegalArity } from "@thi.ng/errors/illegal-arity";
export const DEFAULT = Symbol("DEFAULT");

export type DispatchFn = (...args) => PropertyKey;
export type DispatchFn1<A> = (a: A) => PropertyKey;
export type DispatchFn2<A, B> = (a: A, b: B) => PropertyKey;
export type DispatchFn3<A, B, C> = (a: A, b: B, c: C) => PropertyKey;
export type DispatchFn4<A, B, C, D> = (a: A, b: B, c: C, d: D) => PropertyKey;
export type DispatchFn5<A, B, C, D, E> = (a: A, b: B, c: C, d: D, e: E) => PropertyKey;
export type DispatchFn6<A, B, C, D, E, F> = (a: A, b: B, c: C, d: D, e: E, f: F) => PropertyKey;
export type DispatchFn7<A, B, C, D, E, F, G> = (a: A, b: B, c: C, d: D, e: E, f: F, g: G) => PropertyKey;
export type DispatchFn8<A, B, C, D, E, F, G, H> = (a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H) => PropertyKey;

export type Implementation<T> = (...args: any[]) => T;
export type Implementation1<A, T> = (a: A) => T;
export type Implementation2<A, B, T> = (a: A, b: B) => T;
export type Implementation3<A, B, C, T> = (a: A, b: B, c: C) => T;
export type Implementation4<A, B, C, D, T> = (a: A, b: B, c: C, d: D) => T;
export type Implementation5<A, B, C, D, E, T> = (a: A, b: B, c: C, d: D, e: E) => T;
export type Implementation6<A, B, C, D, E, F, T> = (a: A, b: B, c: C, d: D, e: E, f: F) => T;
export type Implementation7<A, B, C, D, E, F, G, T> = (a: A, b: B, c: C, d: D, e: E, f: F, g: G) => T;
export type Implementation8<A, B, C, D, E, F, G, H, T> = (a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H) => T;

export interface MultiFn<T> extends Implementation<T> {
add: (id: PropertyKey, g: Implementation<T>) => boolean;
remove: (id: PropertyKey) => boolean;
}

export interface MultiFn1<A, T> extends Implementation1<A, T> {
add: (id: PropertyKey, g: Implementation1<A, T>) => boolean;
remove: (id: PropertyKey) => boolean;
}

export interface MultiFn2<A, B, T> extends Implementation2<A, B, T> {
add: (id: PropertyKey, g: Implementation2<A, B, T>) => boolean;
remove: (id: PropertyKey) => boolean;
}

export interface MultiFn3<A, B, C, T> extends Implementation3<A, B, C, T> {
add: (id: PropertyKey, g: Implementation3<A, B, C, T>) => boolean;
remove: (id: PropertyKey) => boolean;
}

export interface MultiFn4<A, B, C, D, T> extends Implementation4<A, B, C, D, T> {
add: (id: PropertyKey, g: Implementation4<A, B, C, D, T>) => boolean;
remove: (id: PropertyKey) => boolean;
}

export interface MultiFn5<A, B, C, D, E, T> extends Implementation5<A, B, C, D, E, T> {
add: (id: PropertyKey, g: Implementation5<A, B, C, D, E, T>) => boolean;
remove: (id: PropertyKey) => boolean;
}

export interface MultiFn6<A, B, C, D, E, F, T> extends Implementation6<A, B, C, D, E, F, T> {
add: (id: PropertyKey, g: Implementation6<A, B, C, D, E, F, T>) => boolean;
remove: (id: PropertyKey) => boolean;
}

export interface MultiFn7<A, B, C, D, E, F, G, T> extends Implementation7<A, B, C, D, E, F, G, T> {
add: (id: PropertyKey, g: Implementation7<A, B, C, D, E, F, G, T>) => boolean;
remove: (id: PropertyKey) => boolean;
}

export interface MultiFn8<A, B, C, D, E, F, G, H, T> extends Implementation8<A, B, C, D, E, F, G, H, T> {
add: (id: PropertyKey, g: Implementation8<A, B, C, D, E, F, G, H, T>) => boolean;
remove: (id: PropertyKey) => boolean;
}

/**
* Returns a new multi-dispatch function using the provided dispatcher.
* The dispatcher can take any number of arguments and must produce a
Expand All @@ -21,12 +78,26 @@ export interface MultiFn<T> extends Implementation<T> {
* implementation is available, attempts to dispatch to DEFAULT impl. If
* none is registered, an error is thrown.
*
* `defmulti` provides generics for type checking up to 8 args (plus the
* return type) and the generics will also apply to all implementations.
* If more than 8 args are required, `defmulti` will fall back to an
* untyped varargs solution.
*
* Implementations for different dispatch values can be added and
* removed dynamically by calling `.add(id, fn)` or `.remove(id)` on the
* returned function. Each returns `true` if the operation was
* successful.
*/
export function defmulti<T>(f: DispatchFn): MultiFn<T> {
export function defmulti<T>(f: DispatchFn): MultiFn<T>;
export function defmulti<A, T>(f: DispatchFn1<A>): MultiFn1<A, T>;
export function defmulti<A, B, T>(f: DispatchFn2<A, B>): MultiFn2<A, B, T>;
export function defmulti<A, B, C, T>(f: DispatchFn3<A, B, C>): MultiFn3<A, B, C, T>;
export function defmulti<A, B, C, D, T>(f: DispatchFn4<A, B, C, D>): MultiFn4<A, B, C, D, T>;
export function defmulti<A, B, C, D, E, T>(f: DispatchFn5<A, B, C, D, E>): MultiFn5<A, B, C, D, E, T>;
export function defmulti<A, B, C, D, E, F, T>(f: DispatchFn6<A, B, C, D, E, F>): MultiFn6<A, B, C, D, E, F, T>;
export function defmulti<A, B, C, D, E, F, G, T>(f: DispatchFn7<A, B, C, D, E, F, G>): MultiFn7<A, B, C, D, E, F, G, T>;
export function defmulti<A, B, C, D, E, F, G, H, T>(f: DispatchFn8<A, B, C, D, E, F, G, H>): MultiFn8<A, B, C, D, E, F, G, H, T>;
export function defmulti<T>(f: any): MultiFn<T> {
let impls: IObjectOf<Implementation<T>> = {};
let fn: any = (...args) => {
const id = f(...args);
Expand Down Expand Up @@ -54,8 +125,11 @@ export function defmulti<T>(f: DispatchFn): MultiFn<T> {
* also registers a `DEFAULT` implementation which simply throws an
* `IllegalArityError` when invoked.
*
* **Note:** Unlike `defmulti` no argument type checking is supported,
* however you can specify the return type for the generated function.
*
* ```
* const foo = defmultiN({
* const foo = defmultiN<string>({
* 0: () => "zero",
* 1: (x) => `one: ${x}`,
* 3: (x, y, z) => `three: ${x}, ${y}, ${z}`
Expand All @@ -69,12 +143,16 @@ export function defmulti<T>(f: DispatchFn): MultiFn<T> {
* // three: 1, 2, 3
* foo(1, 2);
* // Error: illegal arity: 2
*
* foo.add(2, (x, y) => `two: ${x}, ${y}`);
* foo(1, 2);
* // two: 1, 2
* ```
*
* @param impls
*/
export function defmultiN<T>(impls: { [id: number]: Implementation<T> }) {
const fn = defmulti((...args: any[]) => args.length);
const fn = defmulti<T>((...args: any[]) => args.length);
fn.add(DEFAULT, (...args) => illegalArity(args.length));
for (let id in impls) {
fn.add(id, impls[id]);
Expand Down

0 comments on commit eeed25e

Please sign in to comment.