UNPKG

sum-type

Version:

A simple library for complex logic

503 lines (451 loc) 10.4 kB
/* globals Symbol */ export 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) } export 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 }), {}) } } export const _fold = () => fns => M => fns[M.tag](M.value) export const pipe = (...fns) => { if (fns.length == 0) { throw new Error('You must provide a non-empty spread of functions.') } return initial => run(initial, ...fns) } export const repeat = (n, x) => Array(n).fill(x) export const repeatStr = (n, x) => n > 0 ? repeat(n, x).join('') : '' export 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 }) } export 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, ) export 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) export function boundToJSON() { return toJSON(this) } export function boundToString() { return toString(this) } export function typeName(instance) { return instance.type } export function tagName(instance) { return instance.tag } export const proto = { toString: boundToString ,inspect: boundToString // , toJSON: boundToJSON ,[Symbol.for('nodejs.util.inspect.custom')]: boundToString } export function valueInstance(type, tag, value) { return Object.assign(Object.create(proto), { type ,tag ,value, }) } export function emptyInstance(type, tag) { return Object.assign(Object.create(proto), { type ,tag }) } export const fromEntries = pairs => pairs.reduce((p, [k, v]) => ({ ...p, [k]: v }), {}) export 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: {} }, ) export function getTags(T) { return T.tags } export 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 ;[ 'Your tag function must have exactly the same' ,'keys as the type: ' + o.T.type + '. The following keys were' ,'missing:' ,o.missingKeys.join(', ') ,].join(' ') } ,InstanceNull: function InstanceNull(o) { return 'Null is not a valid member of the type ' + o.T.type } ,InstanceWrongType: function InstanceWrongType(o) { return ;[ toString(o.x) + ' is not a valid member of the type' ,o.T.type ,'which expects the following tags' ,getTags(o.T).join(' | ') ,].join(' ') } ,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) ) }, } export 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) } } } } export const errMessage = err => _fold(StaticSumTypeError)(ErrMessageTags)(err) export function handleError(err) { const e = new Error(tagName(err) + ': ' + errMessage(err)) throw e } export const Err = StaticSumTypeError export 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 } export function assertValidVisitor(o) { if (typeof o.visitor != 'function') { return handleError( Err.VisitorNotAFunction({ context: o.context, visitor: o.visitor }), ) } return null } export 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 } export 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) } } export 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 } export function fold(T) { return foldT(() => T) } export 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) } } } export 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 } } } } export 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) ,]), ), } } export function either(type) { return tags(type, ['Y', 'N']) } export const Either = either('sumType.Either')