Last active
October 22, 2015 14:30
-
-
Save mcsf/03404b42a98ae2067dfb to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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