inquiry-monad
Version:
Inquiry is an expressive API that allows one ask multiple questions about a subject value, and observe all results. This process returns a collection of all passes, fails, and the original subject value.
635 lines • 25.2 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const simple_maybe_1 = require("simple-maybe");
const symbols_1 = require("./symbols");
exports.$$inquirySymbol = symbols_1.$$inquirySymbol;
exports.$$questionSymbol = symbols_1.$$questionSymbol;
exports.$$passSymbol = symbols_1.$$passSymbol;
exports.$$failSymbol = symbols_1.$$failSymbol;
const noop = () => { };
const $$notFoundSymbol = Symbol('Not found');
// like Promise.all without the fast reject functionality
const PromiseEvery = (promises) => new Promise((resolve) => {
const results = [];
let count = 0;
promises.forEach((promise, idx) => {
promise
.catch(err => err) // pass errs down as (presumably) Fail
.then((valueOrError) => {
results[idx] = valueOrError;
count += 1;
count === promises.length && resolve(results);
});
});
});
const IOU = (x) => ({
map: (f) => IOU(f(x)),
chain: (f) => f(x),
ap: (y) => y.map(x),
inspect: () => `IOU(${x})`,
join: () => x,
emit: () => x,
concat: (o) => o.chain((r) => IOU(x.concat(r))),
head: () => Array.isArray(x) && x.length ? x[0] : [],
tail: () => Array.isArray(x) && x.length ? x[x.length - 1] : [],
isEmpty: () => Boolean(!Array.isArray(x) || x.length === 0),
[symbols_1.$$inquirySymbol]: false,
[symbols_1.$$passSymbol]: false,
[symbols_1.$$failSymbol]: false,
[symbols_1.$$iouSymbol]: true
});
exports.IOU = IOU;
const Pass = (x) => ({
map: (f) => Pass(f(x)),
chain: (f) => f(x),
fold: (f, _) => f(x),
fork: (_, f) => f(x),
head: () => (Array.isArray(x) && x.length ? x[0] : []),
tail: () => Array.isArray(x) && x.length ? x[x.length - 1] : [],
join: () => x,
emit: () => x,
inspect: () => `Pass(${x})`,
concat: (o) => o.fold((r) => Pass(x.concat(r)), noop),
ap: (y) => y[symbols_1.$$passSymbol] ? y.concat(Pass(x)) : Pass(x),
answer: (i, n = '(anonymous)', Ix = Inquiry) => {
i.informant([n, Pass(x)]);
return Ix({
subject: i.subject,
fail: i.fail,
iou: i.iou,
pass: i.pass.concat(Pass(x)),
informant: i.informant,
questionset: i.questionset,
receipt: i.receipt.concat(Receipt([[n, Pass(x)]]))
});
},
isEmpty: () => Boolean(!Array.isArray(x) || x.length === 0),
[symbols_1.$$passSymbol]: true,
[symbols_1.$$failSymbol]: false,
[symbols_1.$$iouSymbol]: false,
[symbols_1.$$inquirySymbol]: false
});
exports.Pass = Pass;
const Fail = (x) => ({
map: (f) => Fail(f(x)),
chain: (f) => f(x),
fold: (_, f) => f(x),
fork: (f, _) => f(x),
head: () => (Array.isArray(x) && x.length ? x[0] : []),
tail: () => Array.isArray(x) && x.length ? x[x.length - 1] : [],
join: () => x,
emit: () => x,
inspect: () => `Fail(${x})`,
concat: (o) => o.fork((r) => Fail(x.concat(r)), noop),
ap: (y) => y[symbols_1.$$passSymbol] ? Fail(x) : y.concat(Fail(x)),
answer: (i, n = '(anonymous)', Ix = Inquiry) => {
i.informant([n, Fail(x)]);
return Ix({
subject: i.subject,
fail: i.fail.concat(Fail(x)),
pass: i.pass,
iou: i.iou,
informant: i.informant,
questionset: i.questionset,
receipt: i.receipt.concat(Receipt([[n, Fail(x)]]))
});
},
isEmpty: () => Boolean(!Array.isArray(x) || x.length === 0),
[symbols_1.$$passSymbol]: false,
[symbols_1.$$failSymbol]: true,
[symbols_1.$$iouSymbol]: false,
[symbols_1.$$inquirySymbol]: false
});
exports.Fail = Fail;
const Receipt = (x) => ({
map: (f) => Receipt(f(x)),
chain: (f) => f(x),
fold: (_, f) => f(x),
fork: (f, _) => f(x),
head: () => x[0],
tail: () => x[x.length - 1],
join: () => x,
emit: () => x,
inspect: () => `Receipt(${x})`,
isEmpty: () => Boolean(!Array.isArray(x) || x.length === 0),
concat: (o) => o.chain((r) => Receipt(x.concat(r))),
ap: (y) => y.map(x),
[symbols_1.$$inquirySymbol]: false,
[symbols_1.$$receiptSymbol]: true
});
exports.Receipt = Receipt;
const questionTypeError = (x) => console.error('Question must be passed parameters that adhere to the documented type. Value that was passed:', x);
const Question = (x) => ({
map: (f) => Question(f(x)),
chain: (f) => f(x),
ap: (y) => y.map(x),
inspect: () => `Question(${x})`,
join: () => x,
emit: () => x,
call: (i) => x[1](i.join().subject.join()),
extract: () => x[1],
name: () => x[0],
[symbols_1.$$questionSymbol]: true
});
const QuestionOf = (x) => Array.isArray(x) ? Question(x) : questionTypeError(x);
const exportQuestion = {
of: QuestionOf
};
exports.Question = exportQuestion;
const Questionset = (x) => ({
map: (f) => Questionset(f(x)),
chain: (f) => f(x),
ap: (y) => y.map(x),
inspect: () => `Questionset(${x})`,
join: () => x,
emit: () => x,
concat: (o) => o.chain((r) => Questionset(x.concat(r))),
find: (a) => simple_maybe_1.Maybe.of(x.find(i => RegExp(i[0]).test(a)))
.map((b) => b[1])
.fork(() => {
console.warn('Question was not found: ', a);
return $$notFoundSymbol;
}, (c) => c),
[symbols_1.$$questionsetSymbol]: true
});
const questionsetTypeError = (x) => console.error('Questionset must be passed parameters that adhere to the documented type. Value that was passed:', x);
const QuestionsetOf = (x) => Array.isArray(x) ? Questionset(x) : questionsetTypeError(x);
const exportQuestionset = {
of: QuestionsetOf
};
exports.Questionset = exportQuestionset;
const InquirySubject = (x) => x[symbols_1.$$inquirySymbol]
? x
: Inquiry({
subject: simple_maybe_1.Maybe.of(x),
fail: Fail([]),
pass: Pass([]),
iou: IOU([]),
informant: (_) => _,
questionset: Questionset([['', noop]]),
receipt: Receipt([])
});
const warnTypeError = (x) => {
console.warn('Inquiry.of requires properties: subject, fail, pass, iou, informant, questionset, receipt. Converting to Inquiry.subject().');
return InquirySubject(x);
};
// @todo validate constructor via Symbol
const InquiryOf = (x) => 'subject' in x &&
'fail' in x &&
'pass' in x &&
'iou' in x &&
'informant' in x &&
'questionset' in x &&
'receipt' in x
? Inquiry(x)
: warnTypeError(x);
const Inquiry = (x) => ({
// Inquire: core method
// You may pass a Function, a QuestionMonad (with a function), or a string which will look up
// in the current Inquiry's questionset.
// @todo in 1.x, deprecate Function as an option
// @todo should we allow a Questionset to be passed here, and process all of them?
// @todo after simplifying, can BoolTable help here?
inquire: (f) => {
const extractName = (f) => f[symbols_1.$$questionSymbol]
? f.name()
: f;
const fnName = typeof f === 'function' ? f.name || 'anon' : extractName(f);
const fExtractFn = f[symbols_1.$$questionSymbol]
? f.extract()
: f;
const fIsFn = typeof fExtractFn === 'function';
const inquire = fIsFn
? fExtractFn
: x.questionset.find(fExtractFn);
const warnNotPassFail = (resp) => {
console.warn('inquire was passed a function that does not return Pass or Fail:', fnName);
console.warn('response was:', resp);
return Inquiry(x);
};
const inquireResponse = typeof inquire === 'function' ? inquire(x.subject.join()) : {};
return inquireResponse[symbols_1.$$failSymbol] ||
inquireResponse[symbols_1.$$passSymbol] ||
inquireResponse[symbols_1.$$inquirySymbol]
? inquireResponse.answer(x, fnName, Inquiry)
: warnNotPassFail([inquireResponse, fnName]);
},
inquireMap: (f, i) => i.reduce((inq, ii) => {
const extractName = (f) => f[symbols_1.$$questionSymbol]
? f.name()
: f;
const fnName = typeof f === 'function' ? f.name || 'anon' : extractName(f);
const fExtractFn = f[symbols_1.$$questionSymbol]
? f.extract()
: f;
const fIsFn = typeof fExtractFn === 'function';
const inquire = fIsFn
? fExtractFn
: x.questionset.find(fExtractFn);
const warnNotPassFail = (resp) => {
console.warn('inquire was passed a function that does not return Pass or Fail:', fnName);
console.warn('response was:', resp);
return inq;
};
const inquireResponse = typeof inquire === 'function'
? inquire(ii)(inq.join().subject.join())
: {};
// each return aggregates new contained value through exit
return inquireResponse[symbols_1.$$failSymbol] ||
inquireResponse[symbols_1.$$passSymbol] ||
inquireResponse[symbols_1.$$inquirySymbol]
? inquireResponse.answer(inq.join(), fnName, Inquiry)
: warnNotPassFail([inquireResponse, fnName]);
},
// initial Inquiry will be what is in `x` now
Inquiry({
subject: x.subject,
iou: x.iou,
fail: x.fail,
pass: x.pass,
informant: x.informant,
questionset: x.questionset,
receipt: x.receipt
})),
// @todo handle if no Questionset / .using()
inquireAll: () => x.questionset.chain((questions) => questions.reduce((inq, q) => inq.inquire(QuestionOf(q)), Inquiry(x))),
using: (a) => Inquiry({
subject: x.subject,
iou: x.iou,
fail: x.fail,
pass: x.pass,
informant: x.informant,
questionset: a,
receipt: x.receipt
}),
// Informant: for spying/logging/observable
informant: (f) => Inquiry({
subject: x.subject,
iou: x.iou,
fail: x.fail,
pass: x.pass,
informant: f,
questionset: x.questionset,
receipt: x.receipt
}),
inspect: () => `Inquiry(${x.fail.inspect()} ${x.pass.inspect()} ${x.iou.inspect()}`,
// Flow control: swap pass/fail
swap: () => Inquiry({
subject: x.subject,
iou: x.iou,
fail: Fail(x.pass.join()),
pass: Pass(x.fail.join()),
informant: x.informant,
questionset: x.questionset,
receipt: x.receipt
}),
// Mapping across both branches
unison: (f) => // apply a single map to both fail & pass (e.g. sort)
Inquiry({
subject: x.subject,
iou: x.iou,
fail: Fail(f(x.fail.join())),
pass: Pass(f(x.pass.join())),
informant: x.informant,
questionset: x.questionset,
receipt: x.receipt
}),
// standard Monad methods
map: (f) => InquirySubject(f(x)),
ap: (y) => y.map(x),
chain: (f) => f(x),
join: () => x,
emit: () => x,
// execute the provided function if there are failures, else continue
breakpoint: (f) => x.fail.join().length ? Inquiry(f(x)) : Inquiry(x),
// execute the provided function if there are passes, else continue
milestone: (f) => x.pass.join().length ? Inquiry(f(x)) : Inquiry(x),
// internal method: execute informant, return new InquiryP() based on updated results
answer: (i, n, _) => {
i.informant([n, Inquiry(x)]);
return Inquiry({
subject: i.subject,
iou: i.iou,
fail: i.fail.concat(x.fail),
pass: i.pass.concat(x.pass),
informant: i.informant,
questionset: i.questionset,
receipt: i.receipt
});
},
// Unwrap methods
// unwraps, mapping for both branches, full value returned
conclude: (f, g) => ({
subject: x.subject,
iou: x.iou,
fail: f(x.fail),
pass: g(x.pass),
informant: x.informant,
questionset: x.questionset,
receipt: x.receipt
}),
// If there are no fails, handoff aggregated passes to supplied function; if any fails, return noop
cleared: (f) => x.fail.isEmpty() ? f(x.pass) : noop(),
// If there are fails, handoff aggregated fails to supplied function; if no fails, return noop
faulted: (f) => x.fail.isEmpty() ? noop() : f(x.fail),
// If there are passes, handoff aggregated passes to supplied function; if no passes, return noop
suffice: (f) => x.pass.isEmpty() ? noop() : f(x.pass),
// If there are no passes, handoff aggregated fails to supplied function; if any passes, return noop
scratch: (f) => x.pass.isEmpty() ? f(x.fail) : noop(),
// unwrap left if any fails, right if not
fork: (f, g) => x.fail.join().length ? f(x.fail) : g(x.pass),
// unwrap left if any passes, right if not
fold: (f, g) => x.pass.join().length ? f(x.pass) : g(x.fail),
// return a merged pass/fail
zip: (f) => f(x.fail.join().concat(x.pass.join())),
[symbols_1.$$inquirySymbol]: true
});
const exportInquiry = {
subject: InquirySubject,
of: InquiryOf
};
exports.Inquiry = exportInquiry;
const InquiryPSubject = (x) => x[symbols_1.$$inquirySymbol]
? x
: InquiryP({
subject: simple_maybe_1.Maybe.of(x),
fail: Fail([]),
pass: Pass([]),
iou: IOU([]),
informant: (_) => _,
questionset: Questionset([['', noop]]),
receipt: Receipt([])
});
const warnTypeErrorP = (x) => {
console.warn('InquiryP.of requires properties: subject, fail, pass, iou, informant, questionset, receipt. Converting to InquiryP.subject().');
return InquiryPSubject(x);
};
const InquiryPOf = (x) => 'subject' in x &&
'fail' in x &&
'pass' in x &&
'iou' in x &&
'informant' in x &&
'questionset' in x &&
'receipt' in x
? InquiryP(x)
: warnTypeErrorP(x);
const buildInq = (x) => (vals) => vals
? vals.reduce((acc, cur) => cur ? cur[1].answer(acc, cur[0], InquiryP).join() : acc, x)
: x;
// this is a bit complex, so here it goes:
// Take all our IOUs (Questions), extract and resolve their Promises
// then take those results apply to a tuple with the question name/description
const resolveQs = (x) => x.iou.join().length
? x.iou.join().map((q) => q
.extract()()
.catch((err) => err)
.then((result) => Promise.resolve([q.name(), result])))
: [Promise.resolve()];
const InquiryP = (x) => ({
inquire: (f) => {
const extractName = (f) => f[symbols_1.$$questionSymbol]
? f.name()
: f;
const fnName = typeof f === 'function' ? f.name || 'anon' : extractName(f);
const fExtractFn = f[symbols_1.$$questionSymbol]
? f.extract()
: f;
const fIsFn = typeof fExtractFn === 'function';
const inquire = fIsFn
? fExtractFn
: x.questionset.find(fExtractFn);
const warnNotPassFail = (resp) => {
console.warn('inquire was passed a function that does not return Pass or Fail:', fnName);
console.warn('response was:', resp);
return InquiryP(x);
};
const inquireResponse = typeof inquire === 'function' ? inquire(x.subject.join()) : {};
const syncronousResult = (response) => response[symbols_1.$$failSymbol] ||
response[symbols_1.$$passSymbol] ||
response[symbols_1.$$inquirySymbol]
? response.answer(x, fnName, InquiryP)
: warnNotPassFail([inquireResponse, fnName]);
const inquireIOU = inquireResponse.then
? QuestionOf([fnName, () => inquireResponse])
: false;
return inquireIOU
? InquiryP({
subject: x.subject,
fail: x.fail,
pass: x.pass,
iou: x.iou.concat(IOU([inquireIOU])),
informant: x.informant,
questionset: x.questionset,
receipt: x.receipt
})
: syncronousResult(inquireResponse);
},
inquireMap: (f, i) => i.reduce((inq, ii) => {
const extractName = (f) => f[symbols_1.$$questionSymbol]
? f.name()
: f;
const fnName = typeof f === 'function' ? f.name || 'anon' : extractName(f);
const fExtractFn = f[symbols_1.$$questionSymbol]
? f.extract()
: f;
const fIsFn = typeof fExtractFn === 'function';
const inquire = fIsFn
? fExtractFn
: x.questionset.find(fExtractFn);
const warnNotPassFail = (resp) => {
console.warn('inquire was passed a function that does not return Pass or Fail:', fnName);
console.warn('response was:', resp);
return inq;
};
const inquireResponse = typeof inquire === 'function'
? inquire(ii)(inq.join().subject.join())
: {};
const syncronousResult = (response) => response[symbols_1.$$failSymbol] ||
response[symbols_1.$$passSymbol] ||
response[symbols_1.$$inquirySymbol]
? response.answer(inq.join(), fnName, InquiryP)
: warnNotPassFail(response);
return inquireResponse.then
? InquiryP({
subject: inq.join().subject,
fail: inq.join().fail,
pass: inq.join().pass,
iou: inq.join().iou.concat(IOU([inquireResponse])),
informant: inq.join().informant,
questionset: inq.join().questionset,
receipt: inq.join().receipt
})
: syncronousResult(inquireResponse);
},
// initial Inquiry will be what is in `x` now
InquiryP({
subject: x.subject,
iou: x.iou,
fail: x.fail,
pass: x.pass,
informant: x.informant,
questionset: x.questionset,
receipt: x.receipt
})),
inquireAll: () => x.questionset.chain((questions) => questions.reduce((inq, q) => inq.inquire(QuestionOf(q)), InquiryP(x))),
using: (a) => InquiryP({
subject: x.subject,
iou: x.iou,
fail: x.fail,
pass: x.pass,
informant: x.informant,
questionset: a,
receipt: x.receipt
}),
// Informant: for spying/logging/observable
informant: (f) => InquiryP({
subject: x.subject,
iou: x.iou,
fail: x.fail,
pass: x.pass,
informant: f,
questionset: x.questionset,
receipt: x.receipt
}),
inspect: () => `InquiryP(${x.fail.inspect()} ${x.pass.inspect()} ${x.iou.inspect()}`,
// Flow control: swap left/right pass/fail (iou is untouched)
swap: () => InquiryP({
subject: x.subject,
iou: x.iou,
fail: Fail(x.pass.join()),
pass: Pass(x.fail.join()),
informant: x.informant,
questionset: x.questionset,
receipt: x.receipt
}),
// Mapping across both branches
unison: (f) => // apply a single map to both fail & pass (e.g. sort), iou untouched
InquiryP({
subject: x.subject,
iou: x.iou,
fail: Fail(f(x.fail.join())),
pass: Pass(f(x.pass.join())),
informant: x.informant,
questionset: x.questionset,
receipt: x.receipt
}),
// Standard monad methods - note that while these work, remember that `x` is a typed Object
map: (f) => InquiryPSubject(f(x)),
ap: (y) => y.map(x),
chain: (f) => f(x),
join: () => x,
emit: () => x,
// execute the provided function if there are failures, else continue
breakpoint: (f) => x.fail.join().length ? InquiryP(f(x)) : InquiryP(x),
// execute the provided function if there are passes, else continue
milestone: (f) => x.pass.join().length ? InquiryP(f(x)) : InquiryP(x),
// internal method: execute informant, return new InquiryP() based on updated results
answer: (i, n, _) => {
i.informant([n, InquiryP(x)]);
return InquiryP({
subject: i.subject,
iou: i.iou,
fail: i.fail.concat(x.fail),
pass: i.pass.concat(x.pass),
informant: i.informant,
questionset: i.questionset,
receipt: i.receipt
});
},
// Unwrapping methods: all return Promises, all complete outstanding IOUs
// Unwraps the Inquiry after ensuring all IOUs are completed
conclude: (f, g) => __awaiter(this, void 0, void 0, function* () {
return PromiseEvery(resolveQs(x))
.then(buildInq(x))
.then(i => (i[symbols_1.$$inquirySymbol] ? i.join() : i))
.then(y => ({
subject: y.subject,
iou: y.iou,
fail: f(y.fail),
pass: g(y.pass),
informant: y.informant,
questionset: y.questionset,
receipt: y.receipt
}));
}),
// If no fails, handoff aggregated passes to supplied function; if fails, return noop
cleared: (f) => __awaiter(this, void 0, void 0, function* () {
return PromiseEvery(resolveQs(x))
.then(buildInq(x))
.then(i => (i[symbols_1.$$inquirySymbol] ? i.join() : i))
.then(y => (y.fail.isEmpty() ? f(y.pass) : noop()))
.catch(err => console.error('err', err));
}),
// If fails, handoff aggregated fails to supplied function; if no fails, return noop
faulted: (f) => __awaiter(this, void 0, void 0, function* () {
return PromiseEvery(resolveQs(x))
.then(buildInq(x))
.then(i => (i[symbols_1.$$inquirySymbol] ? i.join() : i))
.then(y => (y.fail.isEmpty() ? noop() : f(y.fail)));
}),
// If any passes, handoff aggregated passes to supplied function; if no passes, return noop
suffice: (f) => __awaiter(this, void 0, void 0, function* () {
return PromiseEvery(resolveQs(x))
.then(buildInq(x))
.then(i => (i[symbols_1.$$inquirySymbol] ? i.join() : i))
.then(y => (y.pass.isEmpty() ? noop() : f(y.pass)))
.catch(err => console.error('err', err));
}),
// If no passes, handoff aggregated fails to supplied function; if any passes, return noop
scratch: (f) => __awaiter(this, void 0, void 0, function* () {
return PromiseEvery(resolveQs(x))
.then(buildInq(x))
.then(i => (i[symbols_1.$$inquirySymbol] ? i.join() : i))
.then(y => (y.pass.isEmpty() ? f(y.fail) : noop()));
}),
// Take left function and hands off fails if any, otherwise takes right function and hands off passes to that function
fork: (f, g) => __awaiter(this, void 0, void 0, function* () {
return PromiseEvery(resolveQs(x))
.then(buildInq(x))
.then(i => (i[symbols_1.$$inquirySymbol] ? i.join() : i))
.then(y => (y.fail.join().length ? f(y.fail) : g(y.pass)));
}),
// Take left function and hands off fails if any, otherwise takes right function and hands off passes to that function
fold: (f, g) => __awaiter(this, void 0, void 0, function* () {
return PromiseEvery(resolveQs(x))
.then(buildInq(x))
.then(i => (i[symbols_1.$$inquirySymbol] ? i.join() : i))
.then(y => (y.pass.join().length ? f(y.pass) : g(y.fail)));
}),
// return a Promise containing a merged fail/pass resultset array
zip: (f) => __awaiter(this, void 0, void 0, function* () {
return PromiseEvery(resolveQs(x))
.then(buildInq(x))
.then(i => (i[symbols_1.$$inquirySymbol] ? i.join() : i))
.then(y => f(y.fail.join().concat(y.pass.join())));
}),
// await all IOUs to resolve, then return a new Inquiry CONVERTS TO PROMISE!
await: (t = Infinity) => __awaiter(this, void 0, void 0, function* () {
// try: generator function. Each IOU = array in for loop as per https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/yield
const timeLimit = new Promise((resolve, reject) => setTimeout(resolve, t, [
'timeout',
Fail('Promise(s) have timed out')
]));
const awaitPromises = PromiseEvery(resolveQs(x));
return (Promise.race([timeLimit, awaitPromises])
// @ts-ignore
.then(buildInq(x))
.then((i) => (i[symbols_1.$$inquirySymbol] ? i.join() : i))
.then((y) => InquiryPOf(y)));
}),
[symbols_1.$$inquirySymbol]: true
});
const exportInquiryP = {
subject: InquiryPSubject,
of: InquiryPOf
};
exports.InquiryP = exportInquiryP;
//# sourceMappingURL=index.js.map