most-product v2.0.0
Streams of products and coproducts
Playing with streams of products (pairs) and coproducts (eithers).
Example
Let's say you need to conditionally map events that match a predicate. For example, you want to tag all even and odd numbers with "Even" and "Odd".
You can do it declaratively in 3 composable steps: Partition the stream into evens and odds, tag each respectively, reassemble the stream of tagged values.
// The stream of numbers we want to tag
const nums: Stream<number> = // ...
const tagged: Stream<string> = unpartition(mapEither(tagOdd, tagEven, partition(isEven, nums)))
Using pipelining:
const tagged: Stream<string> = nums
|> partition(isEven)
|> mapEither(tagOdd, tagEven)
|> unpartition
In detail
// Tag even numbers with "Even", and odd numbers with "Odd"
// Ideally, we'd write 3 independent functions, and compose
// a solution from them:
// 1. an isEven predicate
// 2. an "Even" tagging function
// 3. an "Odd" tagging function
const isEven = x => x % 2 === 0
const tagEven = x => `Even ${x}`
const tagOdd = x => `Odd ${x}`
// The stream of numbers we want to tag
// We want to end up with a Stream<string>
const nums: Stream<number> = // ...
Attempt 1
One approach is to filter
the same stream twice.
// We have to remember to multicast since we'll end up
// creating 2 streams derived from nums and merging them.
// That kind of "diamond shape" requires multicasting
const multicastNums: Stream<number> = multicast(nums)
const even: Stream<string> = map(tagEven, filter(isEven, multicastNums))
const odd: Stream<string> = map(tagOdd, filter(x => !isEven(x), multicastNums))
const tagged: Stream<string> = merge(even, odd)
That works, but has 2 downsides:
- It has to apply the isEven predicate twice to each event
- It has to use multicast, which is easy to forget
Attempt 2
Another approach is to write a conditional lambda.
// We can be explicit and write a new lambda to conditionally
// map the events
const tagged: Stream<string> = map(x => isEven(x) ? tagEven(x) : tagOdd(x), nums)
This is a reasonable solution, but still leaves room for improvement:
- The "extra" lambda requires us to think about each event value one at a time, rather than only thinking about the
nums
as a whole. - The lambda mentions
x
4 times
Can we be even more declarative and compositional?
Using a coproduct stream
// We can solve the problem in 3 declarative steps:
// 1. partition the stream of numbers into evens and odds
// 2. tag the evens and odds
// 3. "unpartition" the stream containing the tagged evens and odds
const tagged: Stream<string> = unpartition(mapEither(tagOdd, tagEven, partition(isEven, nums)))
This is completely declarative, with no "extra" lambda, and transforms nums
as a whole, rather than being concerned with each event.