A zip component would be similar to zip and zipWith functions on lists, except it would be working on two sources and sending results into a sink. It would have to stop when either of the sources is exhausted.

A merge component would also take two sources and one sink, but they'd all need to have the same type and it would run as long as either source provides more input.

It's not too clear what combinators could apply to these component types.

The fromHandle producer and toHandle consumer should be reading and writing whole blocks instead of individual characters. The easiest way to integrate them with the other components would be through combinators that convert block components to single-character components.

The substring and parseSubstring primitives use the bulk operations pourUntil and getPrefixOf only for the top-level string. Then they switch to the old algorithm that checks for overlaps. This is a kludge, it must be possible to generalize the test definition.

Splitters are not really the best choice for pattern-matching components. There should be a different component type for that purpose. It would still take one source and two sinks of the same type - one for the matching part of the input and the other for the consumed, but unmatched part - but it would not consume any input beyond what it needs to establish the match portion.

Add a Criterion benchmark suite for evaluating the speed of monad-parallel, monad-coroutine and SCC. The former two should be easy, because the existing tests are pretty much already used for benchmarking.