I construct monad transformers as variants of chain
/of
that take an additional argument - a type directory of the outer monad:
const None =
({runOption: null, tag: "None", [Symbol.toStringTag]: "Option"});
const Some = x =>
({runOption: x, tag: "Some", [Symbol.toStringTag]: "Option"});
const optOfT = of => x => of(Some(x));
const optChainT = ({chain, of}) => fmm => mmx =>
chain(mx => {
switch (mx.tag) {
case "None": return of(None);
case "Some": return fmm(mx.runOption);
}
}) (mmx);
const arrOf = x => [x];
const arrChain = fm => xs =>
xs.reduce((acc, x) => arrPushFlat(acc) (fm(x)), []);
const arrPushFlat = xs => ys => {
ys.forEach(x =>
xs.push(x));
return xs;
};
const xs = [Some("foo"), None, Some("bar")];
console.log(
optChainT({chain: arrChain, of: arrOf})
(s => [Some(s.toUpperCase())]) (xs)); // [Some("FOO"), None, Some("BAR")]
So basically a transformer is a handwritten composition of two monads, i.e. it takes two monads and returns a new composite monad, which is thus itself composable. Welcome to composable effects.
But I cannot wrap my head around monad transformers when lazyness comes on the table. What if I want to create a monad transformer for [Task<Option<a>, Error>]
? I'd need a transformer for asynchronous tasks, i.e. a tChainT
, but how would this operator look like?
Here is a mechanical implementation that (AFAIK) illustrates why monads are not composable in a general manner:
const tChainT = ({chain, of}) => fmm => mmx =>
chain(mx =>
tChain(fmm) (mx) // A
) (mmx);
Line A
returns a Task
that when run will eventually yield an Array
of Task
s of Option
s and then will be passed to the given continuation. But I need the result right away.
Here is the part of my Task
implementation that is relevant to the question:
const Task = k =>
({runTask: (res, rej) => k(res, rej), [Symbol.toStringTag]: "Task"});
const tChain = fm => mx =>
Task((res, rej) => mx.runTask(x => fm(x).runTask(res, rej), rej));
const tOf = x => Task((res, rej) => res(x));