DEV Community

Discussion on: Explain (Timeline) Monads Like I'm Five

Collapse
 
glebec profile image
Gabriel Lebec

The special thing is that the timeline sync method can itself return new timelines, and the result is a single merged timeline. The timeline-monad library is not unique in this respect – RxJS has similar capabilities.

To explain more of what a monad is, we really need to cover functors. A functor is some structure whose values can be mapped, leaving the structure itself alone. Examples:

  • Arrays are functors: mapping str => str.length over an array of two elements ['hi', 'sup'] yields another array of two elements [2, 3].
  • Promises are (almost*) functors: mapping str => str.length over a promise for hello yields another promise for 5 (sadly, TC39 named this method .then instead of .map)
  • Trees can be functors: mapping str => str.length over a tree of triangular shape tree('hi', tree('left'), tree('right')) yields another tree of triangular shape tree(2, tree(4), tree(5)).

Timelines are also functors: timelineA.sync(str => str.length) yields another timeline, whose values are published at the same time as timelineA but which are numbers instead of strings. The "timeline" structure is identical, but the values are transformed.

Cool cool, so where's the monad?

Well, a monad is a functor which also allows you to "put a value in the monad" AND (this is the really special bit) lets you fuse nested layers of structure in a "sensible" (law-abiding) way. Examples:

  • Arrays are monads: you can put a single value into an array (v => [v]), and you can flatten nested layers of arrays ([[5], [1, 4], []].flat() yields [5, 1, 4])
  • Promises are (almost*) monads: you can put a single value into a promise (v => Promise.resolve(v)) and you can flatten nested layers of promise (promiseA.then(a => Promise.resolve(a + 1)) yields not a nested promise, but rather just a single-layer promise; put another way, Promise.resolve(Promise.resolve(1)) is equivalent to just Promise.resolve(1)).

Timeline values from this library are also monadic.

// a timeline of timelines? Nope, a *merged* timeline
const threePingsPerClickT = mouseClicksT.sync(_ => createThreePingsT())

If you return a timeline from a timeline.sync callback, the returned timeline from sync is a single-layer aggregation of events "flattened" down into one timeline – the events will be those resulting from the "inner" returned timelines.

*Special note: to be truly functors or monads, structures must obey specific mathematical laws that guarantee their behavior is sensible and dependable. Those laws are out of scope for this post, but unfortunately Promises do not adhere to them, specifically because instead of having separate map and flatten methods, they have a single method then. That means that sometimes, then maps (like a functor) and sometimes, it flattens (like a monad) – meaning it doesn't strictly obey either sets of laws 100% of the time.