12

Choose a primitive

No primitive covers every case, and the essay argues none should. Answer a few questions about your data to find the primitive that fits. The matrix shows how they differ.
Bringing all of these reactive concepts into a single framework gives us an opportunity to tell a coherent story about reactive programming, promotes a better understanding about what tool is right for the job, and obviates the debate over whether any single primitive is a silver bullet.
How many values?
NumberAxisCastCancelInfoControlLossless
Valuesingularspatial—noone way—yes
Iteratorpluralspatialunicastyestwo waypullyes
Promisesingulartemporalbroadcastnoone waypushyes
Tasksingulartemporalunicastyestwo waypushyes
Streampluraltemporalunicastyestwo waypressureyes
Signalpluraltemporalbroadcastnoone waypushno
Behaviorpluraltemporalbroadcastnoone waypollno

Passing one primitive to another

Promise → Task
coerce either way; a task forks into a promise
Signal → Behavior
an observable's next() polls the latest value
Behavior → Signal
sample on a clock, push the samples
Signal → Stream
buffer pushed values so none are lost
Stream → Signal
drop pressure, broadcast each value
Iterator → Stream
Stream.from(iterable) lifts it onto the time axis
Stream → Promise
all(), join(), read(): collect into one value
  • why it mattersThere is a tension between cancelability and robustness. Promises guarantee that consumers and producers can't interfere; tasks and streams cooperate, so they can cancel.
  • why it mattersUse pressure to handle resource contention when every value matters. Use push (signals) or poll (behaviors) to skip states that don't.
  • why it mattersThe primitives are related, so converting between them is cheap. Promises and tasks make a good channel for plural signals and behaviors.