diff --git a/README.md b/README.md index 810e15d..d06f059 100644 --- a/README.md +++ b/README.md @@ -4,33 +4,19 @@ a small functional/monad library # Usage ## Example ```js -import { Identity, Some, None, Left, Right, Ok, Err } from 'kojima' +import { Option, Some, None, Result, Ok, Err } from 'kojima' -const i = Identity(1) -const thou = Identity(1) -i.bind(x => thou.map(y => x + y)) // Identity(2) - -const maybe = Some('thing') +const maybe = Option.of('thing') maybe.isSome() // true -const isnt = maybe.bind(() => None).map(_x => 'other') // None +const isnt = maybe.chain(None).map(_x => 'other') // None isnt.isSome() // false -const either = Right('neutron') - -const value = either.match({ - Right(value) { - return value - }, - Left(value) { - console.error('oh no') - } -}) // 'neutron' - -const result = Ok('3:41am') +const result = Result.of('3:41am') result.isOk() // true -result.bind(() => new Error(-Infinity)) +result.bind(() => Err(new Error(-Infinity))) .map(_x => '4:10am') + .bind(Ok) result.isErr() // true ``` diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..c50e3ed --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "module": "es2020", + "target": "es6", + "lib": ["es2019", "dom"], + "checkJs": true, + "paths": { + "/*": ["./*"] + } + }, + "exclude": [ + "node_modules" + ] +} + diff --git a/src/algebra/common.js b/src/algebra/common.js new file mode 100644 index 0000000..203a93a --- /dev/null +++ b/src/algebra/common.js @@ -0,0 +1,51 @@ +export const Self = Symbol() +export const Value = Symbol() + +export function constant() { return this } + +/** + * @template T, U + * @typedef {(x: T) => U} Morphism + */ + +/** + * @template T + * @typedef {{ [Value]: T }} Set + */ + +/** + * @template T + * @typedef {{ [Self]: (value: T) => Applicative }} Applicative + */ + +/** + * @template T + * @param {T} value + * @returns {Set} + */ +export const Set = value => ({ [Value]: value }) + +/** + * @template T, U + * @param {Morphism} fn + * @returns {U} + */ +export function chain(fn) { + return fn(this[Value]) +} + +export function extract() { + return this[Value] +} + + +/** + * @template T + * @this {Applicative} + * @param {T} value + * @returns {Applicative} + */ +export function of(value) { + return this[Self](value) +} + diff --git a/src/algebra/index.js b/src/algebra/index.js new file mode 100644 index 0000000..9d71a84 --- /dev/null +++ b/src/algebra/index.js @@ -0,0 +1,3 @@ +export { Some, None, Option } from './option.js' +export { Ok, Err, Result } from './result.js' + diff --git a/src/algebra/option.js b/src/algebra/option.js new file mode 100644 index 0000000..07b84db --- /dev/null +++ b/src/algebra/option.js @@ -0,0 +1,128 @@ +import { chain, constant, Self, Value } from './common.js' + +/** @import { Applicative, Morphism, Set } from './common.js' */ + +/** + * @template T + * @typedef {Some | None} Option + */ + +/** + * @template T + * @typedef SomeMethods + * @property {typeof isSome} isSome + * @property {typeof isNone} isNone + * @property {typeof chain} chain + * @property {typeof map} map + * @property {typeof alt} alt + * @property {typeof fold} fold + */ + +/** + * @template T + * @typedef {Applicative & Set & SomeMethods} Some + * @variation 1 + */ + +/** + * @typedef None + * @property {typeof isSome} isSome + * @property {typeof isNone} isNone + * @property {typeof chain} chain + * @property {typeof map} map + * @property {typeof alt} alt + * @property {typeof fold} fold + */ + +/** + * @template T, U + * @this {Option} + * @param {Morphism} fn + * @returns {Option} + */ +function map(fn) { + return this[Self](this.chain(fn)) +} + +/** + * @template T, U + * @this {Option} + * @param {Morphism} fn + * @param {U} acc + * @return {U} + */ +function fold(fn, acc) { + const result = this.map(fn) + return result.isSome() ? result[Value] : acc +} + +/** + * @template T + * @this {Option} + * @param {Option} other + * @returns {Option} + */ +function alt(other) { + return this.isSome() ? this : other +} + +/** + * @template T + * @this {Option} + * @returns {this is Some} + */ +function isSome() { + return this[Self] === Some +} + +/** + * @template T + * @this {Option} + * @returns {this is None} + */ +function isNone() { + return this === none +} + +/** + * @template T + * @param {?T} value + * @variation 2 + * @returns {Some} + */ +export const Some = value => ({ + [Self]: Some, + [Value]: value, + isSome, + isNone, + chain, + map, + alt, + fold +}) + +/** + * @returns {None} + */ +export const None = () => none + +/** @type {None} */ +export const none = ({ + isSome, + isNone, + chain: constant, + map: constant, + alt, + fold +}) + + +/** + * @template T + * @param {?T} value + * @returns {Option} + */ +export const Option = value => Some(value) +Option.of = Option +Option.zero = None + diff --git a/src/algebra/result.js b/src/algebra/result.js new file mode 100644 index 0000000..f387be2 --- /dev/null +++ b/src/algebra/result.js @@ -0,0 +1,122 @@ +import { chain, constant, Self, Value } from './common.js' + +/** @import { Morphism } from './common.js' */ + +/** + * @template T, E + * @typedef {Ok | Err} Result + */ + +/** + * @template T + * @typedef Ok + * @property {typeof isOk} isOk + * @property {typeof isErr} isErr + * @property {typeof chain} chain + * @property {typeof map} map + * @property {typeof alt} alt + * @property {typeof fold} fold + */ + +/** + * @template T + * @typedef Err + * @property {typeof isOk} isOk + * @property {typeof isErr} isErr + * @property {typeof chain} chain + * @property {typeof map} map + * @property {typeof alt} alt + * @property {typeof fold} fold + */ + +/** + * @template T, U, E + * @this {Result} + * @param {Morphism} fn + * @returns {Result} + */ +function map(fn) { + return this[Self](this.chain(fn)) +} + +/** + * @template T, U, E + * @this {Result} + * @param {Morphism>} fn + * @param {U} acc + * @return {U} + */ +function fold(fn, acc) { + const result = this.map(fn) + return result.isOk() ? result[Value] : acc +} + +/** + * @template T, E + * @this {Result} + * @param {Result} other + * @returns {Result} + */ +function alt(other) { + return this.isOk() ? this : other +} + +/** + * @template T, E + * @this {Result} + * @returns {this is Ok} + */ +function isOk() { + return this[Self] === Ok +} + +/** + * @template T, E + * @this {Result} + * @returns {this is Err} + */ +function isErr() { + return this[Self] === Err +} + +/** + * @template T + * @param {?T} value + * @returns {Ok} + */ +export const Ok = value => Object.freeze({ + [Self]: Ok, + [Value]: value, + isOk, + isErr, + chain, + map, + alt, + fold +}) + +/** + * @template T + * @param {T?} value + * @returns {Err} + */ +export const Err = value => Object.freeze({ + [Self]: Err, + [Value]: value, + chain: constant, + map: constant, + alt, + isOk, + isErr, + fold +}) + +/** + * @template T, E + * @param {T} value + * @returns {Result} + */ +export const Result = value => Ok(value) +Result.of = Result +Result.zero = () => Err(undefined) + diff --git a/src/curry.js b/src/curry.js new file mode 100644 index 0000000..48b3f94 --- /dev/null +++ b/src/curry.js @@ -0,0 +1,73 @@ +/** + * @template {(...args: any[]) => any} F + * @typedef {F extends ((...args: infer A) => any) ? A : never} Params + */ + +/** + * @template {any[]} T + * @typedef {T extends [any, ...any[]] ? T[0] : never} Head + */ + +/** + * @template {any[]} T + * @typedef {((...t: T) => any) extends ((_: any, ...tail: infer TT) => any) ? TT : []} Tail + */ + +/** + * @template {any[]} T + * @typedef {T extends ([] | [any]) ? false : true} HasTail + */ + +/** + * @template {any[]} T + * @typedef {{ 0: Last>, 1: Head }[HasTail extends true ? 0 : 1]} Last + */ + +/** + * @template {any[]} T + * @typedef {T['length']} Length + */ + +/** + * @template E + * @template {any[]} T + * @typedef {((head: E, ...args: T) => any) extends ((...args: infer U) => any) ? U : T} Prepend + */ + +/** + * @template {number} N + * @template {any[]} T + * @template {any[]} [I = []] + * @typedef {{ 0: Drop, Prepend>; 1: T }[Length extends N ? 1 : 0]} Drop + * @typedef {Drop<2, [0, 1, 2, 3, 4]>} t00 + */ + +/** + * @template X, Y + * @typedef {X extends Y ? X : Y} Cast + */ + +/** + * @template {any[]} P, R + * @typedef {(...args: Cast>) => Drop, P> extends [any, ...any[]] ? Curry, P>, any[]>, R> : R} Curry + */ + +/** + * @template {any[]} P, R + * @param {(...args: P) => R} func + * @returns {Curry} + */ +export function curry(func) { + return function curried(...args) { + if (args.length >= func.length) { + return func.apply(this, args) + } else { + return function(...args2) { + return curried.apply(this, args.concat(args2)) + } + } + } +} + + + diff --git a/src/index.js b/src/index.js index 183fd7d..1b504cb 100644 --- a/src/index.js +++ b/src/index.js @@ -1,134 +1,5 @@ -export class MatchError extends Error { - constructor(name) { - super(`Pattern not matched: ${name}`) - } -} - -const ValueKey = Symbol() - -const create = (...args) => Object.freeze(Object.assign(...args)) - -function constant() { return this } -function extract() { return this[ValueKey] } -function extend(f) { return this.of(f(this)) } -function bind(fn) { return fn(this[ValueKey]) } -function ap(other) { return other.map(this[ValueKey]) } -function map(fn) { - return this.of(this.bind(fn)) -} - -function match(pattern) { - const name = this.of.name - - if (name in pattern) { - return this.bind(pattern[name]) - } else if ('_' in pattern) { - return pattern['_']() - } else { - throw new MatchError(name) - } -} - -const Value = v => ({ [ValueKey]: v }) - -const Functor = { - map -} - -const ConstantFunctor = { - map: constant -} - -const ConstantMonad = of => ({ - of, - bind: constant -}) - -const PointedFunctor = of => ({ - of -}) - -const Applicative = { - ...Functor, - ap -} - -const Monad = of => ({ - ...PointedFunctor(of), - bind -}) - -const Comonad = { - extract, - extend -} - -export const Identity = value => create( - Value(value), - Functor, - Monad(Identity) -) - -export const Constant = value => create( - Value(value), - ConstantFunctor, - ConstantMonad(Constant), -) - -export const Ok = value => create( - Value(value), - Functor, - Monad(Ok), - { - isOk: true, - isErr: false, - match - }) - -export const Err = value => create( - Value(value), - ConstantFunctor, - ConstantMonad(Err), - { - isOk: false, - isErr: true, - match - } -) - -export const Some = value => create( - Value(value), - Functor, - Monad(Some), - { - isSome: true, - isNone: false, - match - } -) - -export const None = () => NoneConstant - -const NoneConstant = create( - ConstantFunctor, - ConstantMonad(None), - { - isSome: false, - isNone: true, - match - } -) - -export function curry(func) { - return function curried(...args) { - if (args.length >= func.length) { - return func.apply(this, args) - } else { - return function(...args2) { - return curried.apply(this, args.concat(args2)) - } - } - } -} +import { Result } from './algebra/result.js' +const a = Result.of(2).chain(x => Result.zero()) +console.log(a)