Skip to content

Instantly share code, notes, and snippets.

@mcsf
Last active October 22, 2015 14:30
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mcsf/03404b42a98ae2067dfb to your computer and use it in GitHub Desktop.
Save mcsf/03404b42a98ae2067dfb to your computer and use it in GitHub Desktop.
import R from 'ramda';
import S from 'sanctuary';
import { equal } from 'assert';
// The gist:
function myModule() {
const myStore = createMonadicReducer({
// Plain (a -> b) functions are wrapped so that they return a Maybe
inc: pure(R.inc),
dec: pure(R.dec),
// We can get fancy with `applicative` and get basic type validation
// for free
add: (x, y) => applicative([R.add, x, y]),
// Same, but shorter
addBis: applicativeN(2, R.add),
// Or, we can "manually" return Nothings and Justs. The point of it all
// is that the store will not change state if it's a Nothing.
addSanitized: (x, y) => {
if (y > 1000) return S.Nothing();
return S.Just(x + y);
}
});
// Runs a series of assertions by taking each pair of (action,
// expected-next-state) at a time
testStore(myStore, 0, [
[{ type: 'inc' }, 1],
[{ type: 'inc' }, 2],
[{ type: 'inc' }, 3],
[{ type: 'bogus' }, 3],
[{ type: 'bogus' }, 3],
[{ type: 'dec' }, 2],
[{ type: 'add', data: 10 }, 12],
[{ type: 'add', bogus: 10 }, 12],
[{ type: 'addSanitized', data: 10 }, 22],
[{ type: 'addSanitized', data: 10000 }, 22],
[{ type: 'addBis', data: 20 }, 42],
]);
}
// The implementations:
const createMonadicReducer = fns => {
// getFn :: a -> Maybe f
const getFn = R.compose(
R.chain(R.partialRight(S.get, fns)),
S.get('type')
);
// apply :: Maybe f -> [a, b, ...] -> Maybe c
const apply = (fn, args) =>
R.chain(R.partialRight(R.apply, args), fn);
return (state, action) => {
const fn = getFn(action);
const args = [state, action.data];
return S.fromMaybe(state, apply(fn, args));
};
};
// Turns
// [f, a, b]
// into
// f'.ap(a').ap(b')
// where f', a' and b' are guaranteed to be Maybes
//
// The poor man's applicative wrapper. Its input types are lax and thus untrue
// to the functional way, but we like convenience.
const applicative = R.reduce((acc, term) => {
const maybeTerm = term && term.chain ? term : S.toMaybe(term);
return acc === undefined ?
maybeTerm :
acc.ap(maybeTerm);
}, undefined);
// applicativeN(2, R.add)(4, S.Just(6)) --> S.Just(10)
const applicativeN = (n, fn) => R.curryN(n, (...args) =>
applicative([fn, ...args]));
// Let's hope I'm not abusing the terms. Is this a `pure`?
// pure :: (* -> a) -> (* -> Maybe a)
const pure = R.curryN(2, R.compose)(S.toMaybe);
// Not a very monadic tester, but works
const assert = R.curry((a, b) => equal(a, b));
const testStore = R.curry((store, initialValue, pairs) => {
pairs.map(([ action, expected ]) => R.compose(
R.tap(assert(expected)),
R.flip(store)(action)
)).reduce((prevState, updater) => updater(prevState), initialValue);
console.log('All good!');
});
myModule();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment