Raise multiple errors without throwing.
This is a Fantasy Land compliant monad (maybe) implementing a way to collect multiple errors without aborting the computation. I think this is a real monad, but I have not checked the laws, so maybe it's actually just something pretending to be a monad. Trust at your own discretion.
Anyway, whether or not it's a good monad, this may be a useful construct.
Note: the examples below assume this is published to NPM in a package called
raiser
. It is not. If you actually want to use this library let me know and I'll publish it, but under some other name becauseraiser
is taken and honestly not a great name but it's just what I came up with when I was typing it out.
import Raiser from 'raiser';
This is the core of this whole thing, the Type Representative of the Raiser
monad. This is
really not all that special if you are familiar with the State
monad, and can be understood
as a specialization like this: Raiser e a = State [e] a
.
This is the constructor (e.g. new Raiser
). Not really recommended for use, the underlying
representation of a Raiser
is a function that:
- takes a single parameter
errors :: [e]
, the list of errors that have been raised in the current context; and - returns a record of the form
{ errors, value }
whereerrors :: [e]
is the list of errors with any newly raised ones, andvalue :: a
is the resulting value of this computation.
By using the operators and do-notation helper as provided, you should be able to avoid this constructor easily.
Produces a Raiser
that computes the constant value provided, without raising an errors.
Maps the value of the Raiser
, leaving the errors untouched.
Applies the function which is contained in another Raiser
to the value contained in this
Raiser
. The errors raised by both will be included in the result.
Chains the value of the Raiser, transforming the value while also allowing for new errors to be raised.
Runs a Raiser by calling the underlying function representation that was explained with the constructor. As with the constructor, it's not likely you'll want to use this directly.
import { Errors } from 'raiser';
When a Raiser decides to abort its computation, it will do so by throwing an error of type
Errors
. This value has a property errors
which is an array containing all the values
raised to the Raiser
that is aborting.
I would not be surprised if 100% of the time you are dealing with Raisers, you create
them using doRaiser
and consume them with one of the three functions runRaiser
,
evaluateRaiser
, or tryRaiser
. Though you could use the raw class as described above,
it's really nasty and not a good time.
Builds a Raiser
from a generator in which yield
takes the role of <-
, allowing you
to yield other Raisers
and collect their errors naturally. This is fairly similar to
some other Fantasy Land do-notation things you may see elsewhere, but is included here
to spare you having to find another. Hopefully the examples below show you how this works.
doRaiser
is actually a specialization ofdo_ :: Monad m => TypeRep m -> Generator (forall b. m b) a -> m a
, which you are also welcome to use on any other Fantasy Land compliant monad.
Some things to keep in mind, as Javascript is not that conducive to the do-notation:
- A raiser created this way is single use. You can't run it again.
doRaiser
(and by extension,do_
) will eagerly evaluate up to the first yield. The rest won't be evaluated until you run it withrunRaiser
or whatever. Not a great behaviour I think, but it will do.do_
will not work with non-deterministic monads (e.g. list). See Burrido if you need such a thing, but that's not always great either.
Runs a Raiser
, returning both the list of raised errors and the resulting value.
import { doRaiser, raise, runRaiser } from 'raiser';
const raiser = doRaiser(function* () {
yield raise(new Error('bad'));
return 'ok';
});
runRaiser(raiser); // { errors: [Error('bad')], value: 'ok' }
Runs a Raiser
, ignoring the raised errors and just returning the resulting value.
import { doRaiser, raise, evaluateRaiser } from 'raiser';
const raiser = doRaiser(function* () {
yield raise(new Error('bad'));
return 'ok';
});
evaluateRaiser(raiser); // 'ok'
Runs a Raiser
, aborting after completing the computation if any errors were
raised in the process. Note that this aborts after the computation, so the
return value of the Raiser
will be evaluated anyway, and then discarded.
import { doRaiser, raise, tryRaiser } from 'raiser';
const raiser = doRaiser(function* () {
yield raise(new Error('bad'));
return 'ok';
});
tryRaiser(raiser); // !throws
Raises an error into the Raiser.
import { doRaiser, raise, tryRaiser } from 'raiser';
const raiser = doRaiser(function* () {
yield raise(new Error('bad'));
yield raise(new Error('worse'));
return 'ok';
});
tryRaiser(raiser); // !throws
The opposite of raise
, this will un-raise a previously raised error. If such an error was
not previously raised, nothing will happen.
import { doRaiser, raise, lower, tryRaiser } from 'raiser';
const raiser = doRaiser(function* () {
const error = new Error;
yield raise(error);
yield lower(error);
return 'ok';
});
tryRaiser(raiser); // 'ok'
Gets the current list of errors. This does not modify that list, just lets you access it,
maybe so you can look for ones you know how to handle and lower
them.
import { doRaiser, raise, inspect, runRaiser } from 'raiser';
const raiser = doRaiser(function* () {
yield raise('ok');
const errors = yield inspect;
return errors[0];
});
runRaiser(raiser); // { errors: ['ok'], value: 'ok' }
Gets the current list of errors, and dismisses them all.
import { doRaiser, raise, consume, runRaise } from 'raiser';
const raiser = doRaiser(function* () {
yield raise('ok');
const errors = yield consume;
return errors[0];
});
runRaiser(raiser); // { errors: [], value: 'ok' }
Defines a checkpoint at which, if there are any errors that were previously raised, the computation
is aborted immediately (by throwing an Errors
containing everything that was raised).
Notice that, unless handled along the way, this can cause runRaiser
or evaluateRaiser
to throw,
despite their usual not throwing of unchecked raised errors.
import { doRaiser, raise, consume, evaulateRaiser } from 'raiser';
const raiser = doRaiser(function* () {
yield raise('error');
yield checkpoint;
console.log('This will not happen');
});
runRaiser(raiser); // !throws
Attempts to run another Raiser
, and if that one aborts (due to using checkpoint
), the abort is
stopped and the errors are just put back into the raised state. Since in the case of an abort, there
is no return value available, you must provide a fallback as the first parameter.
import { doRaiser, raise, checkpoint, tryOr, evaluateRaiser } from 'raiser';
const raiser = doRaiser(function* () {
yield raise('error');
const value = yield tryOr('Sorry', doRaiser(function* () {
yield raise('another error');
yield checkpoint;
return 'Success!';
}));
return value;
});
runRaiser(raiser); // { errors: ['error', 'another error'], value: 'Sorry' }