sum-type
Version:
A simple library for complex logic
891 lines (781 loc) • 18.6 kB
JavaScript
(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