sum-type
Version:
A simple library for complex logic
809 lines (702 loc) • 16.4 kB
JavaScript
/* 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;
export { Eall, Eany, EassertN, EassertY, Ebifold, Ebimap, Echain, EchainN, EchainY, EconcatWith, Eencase, EfromNullable, EgetOr, EgetWith, EisN, EisY, decoratedEither as Either, Emap, EmapN, EmapY, Ens, EtagBy, EtoBoolean, Eys, N, StaticSumTypeError, Y, Eall as all, Eany as any, EassertN as assertN, EassertY as assertY, Ebifold as bifold, Ebimap as bimap, boundToString, Echain as chain, chainAll, EchainN as chainN, EchainY as chainY, EconcatWith as concatWith, trait as decorate, decoratedEither, externalEither as either, emptyInstance, Eencase as encase, errMessage, externalEither, externalMaybe, externalTags, fold, fromNullable, EgetOr as getOr, getTags, EgetWith as getWith, EisN as isN, EisY as isY, Emap as map, mapAll, EmapN as mapN, EmapY as mapY, externalMaybe as maybe, Ens as ns, otherwise, pipe, run, EtagBy as tagBy, tagName, externalTags as tags, EtoBoolean as toBoolean, toJSON, toString, typeName, valueInstance, Eys as ys };
//# sourceMappingURL=sum-type.esm.js.map