UNPKG

sum-type

Version:

A simple library for complex logic

891 lines (781 loc) 18.6 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.SumType = {})); }(this, (function (exports) { 'use strict'; /* globals Symbol */ const run = (initial, ...fns) => { if (fns.length == 0) { throw new Error( 'You must provide an initial value and a non-empty spread of functions.', ) } return fns.reduce((p, f) => f(p), initial) }; const otherwise = tagNames => f => { if (Array.isArray(f)) { const sublist = f; const excess = sublist.filter(x => !tagNames.includes(x)); if (excess.length) { throw new Error( 'Tags provided do not match original definition.' + ' The tags: (' + excess.join('|') + ') could not be found in the list:' + ' (' + tagNames.join('|') + ')', ) } else { return otherwise(sublist) } } else { return tagNames.reduce((p, k) => Object.assign(p, { [k]: f }), {}) } }; const _fold = () => fns => M => fns[M.tag](M.value); const pipe = (...fns) => { if (fns.length == 0) { throw new Error('You must provide a non-empty spread of functions.') } return initial => run(initial, ...fns) }; const repeat = (n, x) => Array(n).fill(x); const repeatStr = (n, x) => n > 0 ? repeat(n, x).join('') : ''; function annotate(visitor, value) { const notPrimative = Object(value) === value; const isNil = value == null; const isArray = notPrimative && Array.isArray(value); const isObject = notPrimative && !isArray; const isFunction = notPrimative && typeof value === 'function'; const isPrimative = !notPrimative; const isStag = isObject && value.type && value.tag; const valueStag = isStag && 'value' in value; const emptyStag = isStag && !valueStag; const isPojo = isObject && value.constructor.name == 'Object'; const isDate = isObject && value instanceof Date; const isError = isObject && value instanceof Error; const isString = typeof value === 'string'; return visitor({ notPrimative ,isPrimative ,isString ,isNil ,isArray ,isObject ,isPojo ,isDate ,isError ,value ,isFunction ,isStag ,valueStag ,emptyStag }) } const toString = x => annotate( (function visitor(indentation) { return annotation => { const { value, isPojo, isObject, isArray, isDate, isError, valueStag, emptyStag, isString, isFunction, } = annotation; const indentChar = ''; const tab = repeatStr(indentation, indentChar); const tabLess = repeatStr(indentation - 1, indentChar); const tab2 = repeatStr(indentation + 1, indentChar); const newLine = ''; return valueStag ? value.type + '.' + value.tag + '(' + (typeof value.value === 'undefined' ? '' : annotate(visitor(indentation + 1), value.value)) + (indentation > 0 ? newLine + tabLess : '') + newLine + tab + ')' : emptyStag ? value.type + '.' + value.tag + '()' : isPojo ? Object.keys(value).length == 0 ? '{}' : run( value, Object.entries, xs => xs.map( ([key, value]) => newLine + tab2 + '"' + key + '":' + annotate(visitor(indentation + 1), value).replace( newLine + tab2, '', ), ), strings => newLine + tab + '{' + strings + newLine + tab + '}', ) : isArray ? value.length == '0' ? '[]' : newLine + tab + '[' + newLine + tab + tab + value.map(x => annotate(visitor(indentation + 1), x)).join(', ') + newLine + tab + ']' : isDate ? 'new ' + value.constructor.name + '("' + value.toISOString() + '")' : isError ? 'new ' + value.constructor.name + '("' + value.message + '")' : isFunction ? value + '' : isObject ? 'new ' + value.constructor.name + '()' : isString ? JSON.stringify(value) : '' + value } })(0), x, ); const toJSON = x => annotate(function visitor({ value, isPojo, isArray, valueStag, emptyStag }) { const out = valueStag ? annotate(visitor, value.value) : emptyStag ? null : isPojo ? fromEntries( Object.entries(value).map(value => annotate(visitor, value)), ) : isArray ? value.map(value => annotate(visitor, value)) : value; return out }, x); function boundToString() { return toString(this) } function typeName(instance) { return instance.type } function tagName(instance) { return instance.tag } const proto = { toString: boundToString ,inspect: boundToString // , toJSON: boundToJSON ,[Symbol.for('nodejs.util.inspect.custom')]: boundToString }; function valueInstance(type, tag, value) { return Object.assign(Object.create(proto), { type ,tag ,value, }) } function emptyInstance(type, tag) { return Object.assign(Object.create(proto), { type ,tag }) } const fromEntries = pairs => pairs.reduce((p, [k, v]) => ({ ...p, [k]: v }), {}); const StaticSumTypeError = [ 'ExtraTags' ,'MissingTags' ,'InstanceNull' ,'InstanceWrongType' ,'InstanceShapeInvalid' ,'VisitorNotAFunction' ,'NotAType' ,'TagsShapeInvalid' ,].reduce( (p, n) => { p[n] = value => valueInstance(p.type, n, value); p.tags.push(n); return p }, { type: 'StaticSumTypeError' ,tags: [] ,traits: {} }, ); function getTags(T) { return T.tags } const ErrMessageTags = { ExtraTags: function ExtraTags(o) { return [ 'Your tag function must have exactly the same' ,' keys as the type: ' + o.T.type + '. ' ,'The following tags should not have been present:' ,o.extraKeys.join(', ') ,].join(' ') } ,MissingTags: function MissingTags(o) { return ; } ,InstanceNull: function InstanceNull(o) { return 'Null is not a valid member of the type ' + o.T.type } ,InstanceWrongType: function InstanceWrongType(o) { return ; } ,InstanceShapeInvalid: function InstanceShapeInvalid(o) { return [ toString(o.x) ,'is not a valid Member of the type:' ,o.T.type + '. ' ,'Please review the definition of ' + o.T.type ,].join(' ') } ,VisitorNotAFunction: function (o) { return ( o.context + ' expected a visitor function ' + ' but instead received ' + toString(o.visitor) ) } ,NotAType: function (o) { return ( o.context + ' expected a Type ({ type: string, tags: string[] })' + ' but received ' + toString(o.T) ) } ,TagsShapeInvalid(T, tags) { return ( 'fold(' + typeName(T) + ') tags provided were not the right shape. ' + 'Expected { [tag]: f } but received ' + toString(tags) ) }, }; function foldT(getT) { const T = getT(); assertValidType('fold', T); return function devCata$T(tags) { assertValidCata(T, tags); const tagNames = Object.keys(tags); const tKeys = getTags(T); const xKeys = [ [tagNames, T] ,[tKeys, tags] ,].map(function (t) { const xs = t[0]; const index = t[1]; return xs.filter(function (x) { return !(x in index) }) }); const extraKeys = xKeys[0]; const missingKeys = xKeys[1]; if (missingKeys.length > 0) { return handleError( Err.MissingTags({ T: T, tags, missingKeys: missingKeys }), ) } else if (extraKeys.length > 0) { return handleError(Err.ExtraTags({ T: T, tags, extraKeys: extraKeys })) } else { return function (x) { return beforeFoldEval(T, tags, x) && tags[tagName(x)](x.value) } } } } const errMessage = err => _fold()(ErrMessageTags)(err); function handleError(err) { const e = new Error(tagName(err) + ': ' + errMessage(err)); throw e } const Err = StaticSumTypeError; function assertValidType(context, T) { if ( T == null || !( T != null && typeof T.type == 'string' && Array.isArray(T.tags) && 'traits' in T ) ) { return handleError(Err.NotAType({ context, T })) } return null } function assertValidVisitor(o) { if (typeof o.visitor != 'function') { return handleError( Err.VisitorNotAFunction({ context: o.context, visitor: o.visitor }), ) } return null } function assertValidTag(T, instance) { if ( !( instance != null && typeName(instance) == T.type && getTags(T).includes(tagName(instance)) ) ) { return handleError( Err.InstanceShapeInvalid({ x: instance ,T: T, }), ) } return null } function assertValidCata(T, tags) { if (tags != null && !Array.isArray(tags) && typeof tags === 'object') { return true } else { const err = Err.TagsShapeInvalid(T, tags); return handleError(err) } } function beforeFoldEval(T, tags, x) { return x == null ? handleError( Err.InstanceNull({ T: T ,tags ,x: x, }), ) : typeName(x) !== T.type ? handleError( Err.InstanceWrongType({ T: T ,tags ,x: x, }), ) : !getTags(T).includes(tagName(x)) ? handleError( Err.InstanceShapeInvalid({ T: T ,tags ,x: x, }), ) : true } function fold(T) { return foldT(() => T) } const mapAll = T => { const foldT = fold(T); return tags => { const foldTags = foldT(tags); return Ma => { assertValidTag(T, Ma); const value = foldTags(Ma); return T[tagName(Ma)](value) } } }; const chainAll = T => { const foldT = fold(T); return tags => { const foldTags = foldT(tags); return Ma => { beforeFoldEval(T, tags, Ma); if ('value' in Ma) { const nestedValue = foldTags(Ma); return beforeFoldEval(T, tags, nestedValue) && nestedValue } else { return Ma } } } }; const tags = (type, tagNames) => { return { type ,traits: {} ,tags: tagNames ,...fromEntries( tagNames.map(tagName => [ tagName ,(...args) => args.length ? valueInstance(type, tagName, args[0]) : emptyInstance(type, tagName) ,]), ), } }; function either(type) { return tags(type, ['Y', 'N']) } const Either = either('sumType.Either'); const all = T => Ms => { const bad = Ms.filter(M => !T.toBoolean(M)); if (bad.length > 0) { return T.left(bad.map(x => x.value)) } else { return T.right(Ms.map(T.getOr(null))) } }; const any = T => Ms => Ms.some(T.toBoolean) ? T.right(Ms.filter(T.toBoolean).map(T.getOr(null))) : Ms.find(M => !T.toBoolean(M)); const _bifold = T => { const $fold = fold(T); const { left, right } = T.traits['sum-type/bifunctor']; return (leftF, rightF) => $fold({ [left]: leftF, [right]: rightF }) }; const bifold = T => { assertValidType('bifold', T); return _bifold(T) }; const _bimap = T => { const _mapAll = mapAll(T); const { left, right } = T.traits['sum-type/bifunctor']; return (leftF, rightF) => _mapAll({ [left]: leftF, [right]: rightF }) }; const bimap = T => { assertValidType('bimap', T); return _bimap(T) }; const _getOr = T => or => _bifold(T)( () => or, x => x ); const getOr = T => { assertValidType('getOr', T); return or => { return M => { assertValidTag(T, M); return _getOr(T)(or)(M) } } }; const getWith = T => (otherwise, f) => bifold(T)( () => otherwise, x => f(x) ); const tagBy = T => { fold(T); // just validates T return (otherwise, visitor) => a => { assertValidVisitor({ context: 'tagBy', visitor }); return visitor(a) ? T.right(a) : T.left(otherwise) } }; const toBoolean = T => T.bifold( () => false , () => true ); const fromNullable$1 = T => x => x == null ? T.left(x) : T.right(x); const encase = T => f => x => { try { return T.right(f(x)) } catch (e) { return T.left(e) } }; const _concatWith = T => f => A => B => { const { left, right } = T.traits['sum-type/bifunctor']; return tagName(A) == left ? A : tagName(B) == left ? B : T[right](f(A.value)(B.value)) }; const concatWith = T => { assertValidType('concatWith', T); return f => { assertValidVisitor({ context: 'concatWith' ,visitor: f }); return A => { assertValidTag(T, A); return B => { assertValidTag(T, B); return _concatWith(T)(f)(A)(B) } } } }; const trait$3 = ({ left, right }) => T => { T.traits['sum-type/bifunctor'] = { left, right }; T.left = x => T[left](x); T.right = x => T[right](x); T.bifold = bifold(T); T.bimap = bimap(T); T.getOr = getOr(T); T.getWith = getWith(T); T.tagBy = tagBy(T); T.encase = encase(T); T.toBoolean = toBoolean(T); T.fromNullable = fromNullable$1(T); T.all = all(T); T.any = any(T); T.concatWith = concatWith(T); return T }; const trait$2 = tag => T => { T.traits['sum-type/functor'] = tag; T.of = T[tag]; T.map = T.map || (f => { assertValidVisitor({ context: 'map', visitor: f }); return mapAll(T)({ ...otherwise(T.tags)(x => x) ,[tag]: f, }) }); return T }; const trait$1 = tag => T => { T.traits['sum-type/monad'] = tag; T.of = T[tag]; const chainAllT = chainAll(T); T.chain = f => { assertValidVisitor({ context: 'chain', visitor: f }); return M => { return chainAllT({ ...otherwise(T.tags)(x => T[tagName(M)](x)) ,[tag]: x => f(x), })(M) } }; return T }; function trait(T) { if (T.traits['sum-type/decorated']) { return T } else { // Do not expose this, they need to define their own _ const _ = otherwise(getTags(T)); const mapT = mapAll(T); const chainT = chainAll(T); const foldT = fold(T); getTags(T).forEach(k => { const options = [ [k, k, k] ,[k, '_' + k, '_' + k + '_'] ,]; options.forEach(([tag, fnName, wrapFnName]) => { T['is' + fnName] = T['is' + fnName] || (M => { assertValidTag(T, M); return tagName(M) === tag }); T['map' + fnName] = T['map' + fnName] || (f => mapT({ ..._(x => x) ,[tag]: f, })); T['get' + wrapFnName + 'Or'] = T['get' + wrapFnName + 'Or'] || (otherwise => foldT({ ..._(() => otherwise) ,[tag]: x => x, })); T['get' + wrapFnName + 'With'] = T['get' + wrapFnName + 'With'] || ((otherwise, f) => foldT({ ..._(() => otherwise) ,[tag]: x => f(x), })); T['chain' + fnName] = T['chain' + fnName] || (f => x => chainT({ ..._(() => x) ,[tag]: f, })(x)); T['assert' + fnName] = T['assert' + fnName] || foldT({ ..._(Either.N) ,[tag]: Either.Y, }); T[tag.toLowerCase() + 's'] = T[tag + 's'] || (xs => xs.reduce( (p, n) => p.concat(T['is' + tag](n) ? [n.value] : []), [], )); }); }); T.fold = T.fold || foldT; T.mapAll = T.mapAll || mapT; T.chainAll = T.chainAll || chainT; T.traits['sum-type/decorated'] = true; return T } } const externalEither = name => run( either(name) ,trait ,trait$3({ left: 'N', right: 'Y' }) ,trait$2('Y') ,trait$1('Y') ); const externalMaybe = name => run( name ,externalEither ,x => { const oldN = x.N; x.N = () => oldN(); x.traits['sum-type/maybe'] = true; return x } ); const externalTags = (type, tagNames) => trait(tags(type, tagNames)); const decoratedEither = externalEither('sumType.Either'); const { Y ,N ,bifold: Ebifold ,getOr: EgetOr ,getWith: EgetWith ,bimap: Ebimap ,map: Emap ,mapY: EmapY ,mapN: EmapN ,assertY: EassertY ,assertN: EassertN ,chainN: EchainN ,chainY: EchainY ,tagBy: EtagBy ,chain: Echain ,toBoolean: EtoBoolean ,encase: Eencase ,fromNullable: EfromNullable ,all: Eall ,any: Eany ,isY: EisY ,isN: EisN ,ys: Eys ,ns: Ens ,concatWith: EconcatWith } = decoratedEither; const fromNullable = EfromNullable; exports.Eall = Eall; exports.Eany = Eany; exports.EassertN = EassertN; exports.EassertY = EassertY; exports.Ebifold = Ebifold; exports.Ebimap = Ebimap; exports.Echain = Echain; exports.EchainN = EchainN; exports.EchainY = EchainY; exports.EconcatWith = EconcatWith; exports.Eencase = Eencase; exports.EfromNullable = EfromNullable; exports.EgetOr = EgetOr; exports.EgetWith = EgetWith; exports.EisN = EisN; exports.EisY = EisY; exports.Either = decoratedEither; exports.Emap = Emap; exports.EmapN = EmapN; exports.EmapY = EmapY; exports.Ens = Ens; exports.EtagBy = EtagBy; exports.EtoBoolean = EtoBoolean; exports.Eys = Eys; exports.N = N; exports.StaticSumTypeError = StaticSumTypeError; exports.Y = Y; exports.all = Eall; exports.any = Eany; exports.assertN = EassertN; exports.assertY = EassertY; exports.bifold = Ebifold; exports.bimap = Ebimap; exports.boundToString = boundToString; exports.chain = Echain; exports.chainAll = chainAll; exports.chainN = EchainN; exports.chainY = EchainY; exports.concatWith = EconcatWith; exports.decorate = trait; exports.decoratedEither = decoratedEither; exports.either = externalEither; exports.emptyInstance = emptyInstance; exports.encase = Eencase; exports.errMessage = errMessage; exports.externalEither = externalEither; exports.externalMaybe = externalMaybe; exports.externalTags = externalTags; exports.fold = fold; exports.fromNullable = fromNullable; exports.getOr = EgetOr; exports.getTags = getTags; exports.getWith = EgetWith; exports.isN = EisN; exports.isY = EisY; exports.map = Emap; exports.mapAll = mapAll; exports.mapN = EmapN; exports.mapY = EmapY; exports.maybe = externalMaybe; exports.ns = Ens; exports.otherwise = otherwise; exports.pipe = pipe; exports.run = run; exports.tagBy = EtagBy; exports.tagName = tagName; exports.tags = externalTags; exports.toBoolean = EtoBoolean; exports.toJSON = toJSON; exports.toString = toString; exports.typeName = typeName; exports.valueInstance = valueInstance; exports.ys = Eys; Object.defineProperty(exports, '__esModule', { value: true }); }))); //# sourceMappingURL=sum-type.umd.js.map