JSPM

  • Created
  • Published
  • Downloads 117
  • Score
    100M100P100Q72499F
  • License MIT

A module for chaining testing functions and returning an aggregate of both passes and failures and the original value. Functional, much like an Either.

Package Exports

  • inquiry-monad

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (inquiry-monad) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

Inquiry Monad

v0.16.8

Build Status

Inquiry chains together functions that test a given value ("subject") and return with a full set of all passes, failures, and the original untouched value.

It follows the practices of functional programming, and is monad that is similar to an Either or a Validation.

If you are familiar with Promises, this will look very similar. The regular Inquiry function does not support asyncronous behaviour, however Promises are supported via the InquiryP function. (For the more functionally-minded, Futures are supported via inquiry-monad-futures package, which uses Fluture.)

Basic example

const { Inquiry, InquiryP, Pass, Fail } = require('inquiry-monad');

const subjectData = {
    a: 1,
    b: false
};

const hasA = x => (x.a ? Pass('has a') : Fail('does not have a'));
const validateB = x =>
    x.b && typeof x.b === 'boolean' ? Pass('b is valid') : Fail('b is invalid');
const hasNoC = x => (x.c ? Fail('has a c value') : Pass('has no c value'));

/* With all passes */
Inquiry.subject(subjectData)
    .inquire(hasA)
    .inquire(validateB)
    .inquire(hasNoC)
    .join();

// >> result: {subject: {a:1, b:false}, pass: Pass(['has a', 'b is valid', 'has no c value']), fail: Fail([]), iou: IOU([])}

/* With failures */
const subjectDataWithFailure = {
    a: 1,
    b: 'string',
    c: true
};

Inquiry.subject(subjectDataWithFailure)
    .inquire(hasA)
    .inquire(validateB)
    .inquire(hasNoC)
    .join();

// >> result: {subject: {a:1, b:'string', c:true}, pass: Pass(['has a']), fail: Fail(['b is invalid', 'has c value']), iou: IOU()}

/* With async Promises */
const checkDb = async x =>
    Promise.resolve(Pass('pretend I looked something up in a db'));

InquiryP.subject(subjectDataWithFailure)
    .informant(console.log)
    .inquire(checkDb)
    .inquire(hasA)
    .inquire(validateB)
    .inquire(hasNoC)
    .conclude(x => x, y => y);
// .conclude or another "unwrap" fn is necessary to complete "IOUs" to give a clean exit (resolve all unresolved Promises)

// >> Promise.resolve(result: {subject: {a:1, b:'string', c:true}, pass: Pass(['has a', 'pretend I looked something up in a db']), fail: Fail(['b is invalid', 'has c value']), iou: IOU()})

Description

Inquiry can take any value (a subject), store it within an immutable container (Inquiry or InquiryP monad) to be tested against various functions (via .inquire method) and resulting in two or three lists: Pass([]), Fail([]), and sometimes IOU([]) in the case of InquiryP.

The advantage over traditional Promise chains is that the original subject and each result is retained through the resulting chain of functions, giving complete observability over the data passed through. Also, this allows one to restrain Promises to stay with a monadic structure, bolstering immutibility.

For those who wish to compare to a traditional Left/Right in functional programming, there are many advantages over a Left/Right or Validation pattern from functional:

  • Inquiry aggregates all results, not just failures.
  • Inquiry can run functions against both sides (traditional Left/Right as Fail/Pass)
  • Inquiry retains the original subject rather than transforming it into a result
  • Inquiry is designed to be an expressive, easily understood API

Constructor

Inquiry.subject(value) or InquiryP.subject(value) (Promise/async-based)

Returns an Inquiry monad, which is monad containing an object with properties subject, pass, fail, iou, and an informant method.

subject: contains value passed to Inquiry.subject within a Maybe monad, meaning it will either be Just(value) or Nothing(). pass: contains a Pass monad containing an array of values fail: contains a Fail monad containing an array of values iou: contains an IOU monad contains an array of Promises (only relevant with InquiryP) informant: contains a function to be called upon the return of a .inquire call, for observation or logging purposes (set by calling .informant method)

Using the above object structure, you may also assemble your own Inquiry monad "manually" with Inquiry.of, those this is generally unnecessary.

As a basic example:

const value = { something: true };
console.log(
    Inquiry.subject(value)
        .informant(console.log)
        .join()
); // .join will reveal contained value
// > {subject: Just({something: true}), pass: Pass([]), fail: Fail([]), iou: IOU([]), informant: console.log};

Methods

Core method

.inquire(f) : give inquire a function f that returns either a Pass(), Fail(), Promise (InquiryP only), or another Inquiry. Anything other than these will be assumed as a Pass.

const isMoreThanOne = x =>
    x > 1 ? Pass('Is greater than 1') : Fail('Is less than or equal to 1');

Inquiry.subject(5)
    .inquire(isMoreThanOne)
    .join();
// > {subject: Just(5), pass: Pass(['Is greater than 1']), fail: Fail([]), iou: IOU([]), informant: _ => _};

Interrogative methods:

.inspect() : return a string with the value contained in the Inquiry monad. Used for debugging.

const isMoreThanOne = x =>
    x > 1 ? Pass('Is greater than 1') : Fail('Is less than or equal to 1');

Inquiry.subject(5)
    .inquire(isMoreThanOne)
    .inspect(); // outputs to string
// > Inquiry({subject: Just(5), pass: Pass(['Is greater than 1']), fail: Fail([]), iou: IOU([])});

.informant(f): call function f upon each .inquire result. Useful for logging or observing. The function will be passed an array containing ['fnName', Pass('passed value')] or ['fnName', Fail('failed value')]. Is not run when IOU is added, however does run once the IOU resolves.

const isMoreThanOne = x =>
    x > 1 ? Pass('Is greater than 1') : Fail('Is less than or equal to 1');
const isMoreThanTen = x =>
    x > 10 ? Pass('Is greater than 10') : Fail('Is less than or equal to 10');

Inquiry.subject(5)
    .informant(console.log)
    .inquire(isMoreThanOne)
    .inquire(isMoreThanTen);
// console.log would output:
// > 'isMoreThanOne', Pass('Is greater than 1')
// > 'isMoreThanTen', Fail('Is less than or equal to 10')

Unwrap methods:

.conclude(f, g): returns the full Inquiry's value, with map functions applied to both fail (f) and pass (g), but will wait to resolve all outstanding IOUs (Promises)

const isMoreThanOne = x =>
    x > 1 ? Pass({ greaterThanOne: true }) : Fail({ greaterThanOne: false });
const isMoreThanTen = x =>
    x > 10 ? Pass({ greaterThanTen: true }) : Fail({ greaterThanTen: false });

Inquiry.subject(5)
    .inquire(isMoreThanOne)
    .inquire(isMoreThanTen)
    .conclude(
        x => ({
            failCount: x.join().length,
            fails: x.join()
        }),
        y => ({
            passCount: y.join().length,
            passes: y.join()
        })
    );

// > {subject: Just(5), pass: {passCount: 1, passes: ['Is greater than 1']}, fail: {failCount: 1, fails: ['Is less than or equal to 10']}, iou: IOU([]), informant: _ => _};

.fork(f, g): exit and either run a function f if there are any fails, or g if no fails, returning only result of the function executed.

const isMoreThanOne = x =>
    x > 1 ? Pass({ greaterThanOne: true }) : Fail({ greaterThanOne: false });
const isMoreThanTen = x =>
    x > 10 ? Pass({ greaterThanTen: true }) : Fail({ greaterThanTen: false });

Inquiry.subject(5)
    .inquire(isMoreThanOne)
    .inquire(isMoreThanTen)
    .fork(
        x => ({
            failCount: x.join().length,
            fails: x.join()
        }),
        y => ({
            passCount: y.join().length,
            passes: y.join()
        })
    );

// > {failCount: 1, fails: ['Is less than or equal to 10']}

Inquiry.subject(15)
    .inquire(isMoreThanOne)
    .inquire(isMoreThanTen)
    .fork(
        x => ({
            failCount: x.join().length,
            fails: x.join()
        }),
        y => ({
            passCount: y.join().length,
            passes: y.join()
        })
    );

// > {passCount: 2, passes: ['Is greater than 1', 'Is greater than 10']}

.zip(f): run function f against a merged list of pass and fail

const isMoreThanOne = x =>
    x > 1 ? Pass({ greaterThanOne: true }) : Fail({ greaterThanOne: false });
const isMoreThanTen = x =>
    x > 10 ? Pass({ greaterThanTen: true }) : Fail({ greaterThanTen: false });

Inquiry.subject(5)
    .inquire(isMoreThanOne)
    .inquire(isMoreThanTen)
    .zip(console.log);

// console.log:
// >> [{greaterThanOne: true}, {greaterThanTen: false}]

Early results methods:

.breakpoint(f): run a function f only if fail has contents. f must return an Inquiry.

const isMoreThanOne = x =>
    x > 1 ? Pass({ greaterThanOne: true }) : Fail({ greaterThanOne: false });
const isMoreThanTen = x =>
    x > 10 ? Pass({ greaterThanTen: true }) : Fail({ greaterThanTen: false });
const isMoreThanTwenty = x =>
    x > 20
        ? Pass({ greaterThanTwenty: true })
        : Fail({ greaterThanTwenty: false });

Inquiry.subject(5)
    .inquire(isMoreThanOne)
    .breakpoint(x => {
        console.warn('after one', x.fail.join()); // will not happen
        return Inquiry.of(x);
    })
    .inquire(isMoreThanTen)
    .breakpoint(x => {
        console.warn('after ten', x.fail.join()); // this will run
        return Inquiry.of(x);
    })
    .inquire(isMoreThanTwenty);

.milestone(f): run a function f only if pass has contents. f must return an Inquiry.

const isMoreThanOne = x =>
    x > 1 ? Pass({ greaterThanOne: true }) : Fail({ greaterThanOne: false });
const isMoreThanTen = x =>
    x > 10 ? Pass({ greaterThanTen: true }) : Fail({ greaterThanTen: false });
const isMoreThanTwenty = x =>
    x > 20
        ? Pass({ greaterThanTwenty: true })
        : Fail({ greaterThanTwenty: false });

Inquiry.subject(5)
    .inquire(isMoreThanOne)
    .milestone(x => {
        console.warn('after one', x.pass.join()); // this will run
        return Inquiry.of(x);
    })
    .inquire(isMoreThanTen)
    .milestone(x => {
        console.warn('after ten', x.pass.join()); // this will run (still has passes)
        return Inquiry.of(x);
    })
    .inquire(isMoreThanTwenty);

.await() (InquiryP only): pause and wait for all iou Promises to resolve.

Multi-map method:

.unison(f): run a function f against both pass and fail branches

const isMoreThanOne = x =>
    x > 1 ? Pass('Is greater than 1') : Fail('Is less than or equal to 1');
const isMoreThanTen = x =>
    x > 10 ? Pass('Is greater than 10') : Fail('Is less than or equal to 10');
const allCaps = items => items.map(x => x.toUpperCase());

Inquiry.subject(5)
    .inquire(isMoreThanOne)
    .inquire(isMoreThanTen)
    .unison(allCaps)
    .join();

// > {subject: Just(5), pass: Pass(['IS GREATER THAN 1']), fail: Fail(['IS LESS THANK OR EQUAL TO 10']), iou: IOU([]), informant: _ => _};

Flow-control method:

.swap(): swap the pass and fail branches

// example forthcoming

Standard monadic methods:

Documentation forthcoming for the following.

ap map chain join

Development

Source is written in TypeScript. Run tests via npm run test.

MIT License

Copyright 2018 Robert Gerald Porter rob@weeverapps.com

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.