18 Pragmatic aspects of plumbing

Having seen a basic set of stream-processor combinators--which we can consider as a complete set of primitives on top of which further combinators can be built--we now take a look at how the combinators can be used to achieve some common connection patterns and introduces some further combinators we have found useful.

Fudgets are composed in the same way as plain stream processors. Therefore, the description of the stream-processor combinators also holds true for the corresponding fudget combinators. The fudget combinators are presented by name, together with some further combinators, in Chapter 13.

18.1 Handling multiple input and output streams

Although stream processors have only one input stream, it is easy to construct programs where one stream processor receives input from two or more other stream processors. (The case with several outputs is analogous.) For example, the expression

sp1 -==- (sp2 -+- sp3)
allows sp1 to receive input from both sp2 and sp3. For most practical purposes, sp1 can be regarded as having two input streams, as illustrated in Figure 33. When you use getSP in sp1 to read from the input streams, messages from sp2 and sp3 will appear tagged with Left and Right, respectively. You can not directly read selectively from one of the two input streams, but the Fudget library provides the combinator

Figure 33. Handling multiple input streams.

waitForSP :: (i -> Maybe i') -> (i' -> SP i o) -> SP i o
which you can use to wait for a selected input. Other input is queued and can be consumed after the selected input has been received. Using waitForSP you can define combinators to read from one of two input streams:

getLeftSP :: (i1 -> SP (Either i1 i2) o) -> SP (Either i1 i2) o
getLeftSP = waitForSP stripLeft
getRightSP :: (i2 -> SP (Either i1 i2) o) -> SP (Either i1 i2) o
getRightSP = waitForSP stripRight
Example:
Implement startupSP :: [i] -> SP i o -> SP i o that prepends some elements to the input stream of a stream processor.
Solution:
startupSP xs sp = sp -==- putListSP xs idSP
Note: this implementation leaves a serial composition with idSP behind after the messages xs have been fed to sp. An efficient implementation that does not leave any overhead behind can be obtained by making use of the actual representation of stream processors.
Example:
Implement waitForSP described above.
Solution:
waitForSP :: (i -> Maybe i') -> (i' -> SP i o) -> SP i o
waitForSP expected isp =
    let contSP pending =
          getSP $ \ msg ->
          case expected msg of
            Just answer  -> startupSP (reverse pending) (isp answer)
            Nothing      -> contSP (msg : pending)

    in  contSP []

18.2 Stream processors and software reuse

For serious applications programming, it is useful to have libraries of reusable software components. But in many cases when a useful component is found in a library, it still needs modification before it can be used.

A variation of the loop combinators that has turned out to be very useful when reusing stream processors is loopThroughRightSP, illustrated in Figure 34. The key difference from loopSP and loopLeftSP is that the loop does not go directly back from the output to the input of a single stream processor. Instead it goes through another stream processor.

Figure 34. Encapsulation.

A typical situation where loopThroughRightSP is useful is when you have a stream processor, spold, that does almost what you want it to do, but you need it to handle some new kind of messages. A new stream processor, spnew, can then be defined. This new stream processor can pass on old messages directly to spold and handle the new messages in the appropriate way; on its own, or by translating them to messages that spold understands. (See also Section 3.1.1 in [NR94].)

In the composition loopThroughRightSP spnew spold, all communication with the outside world is handled by spnew. spold is connected only to spnew, and is in this sense encapsulated inside spnew.

The type of loopThroughRightSP is:

loopThroughRightSP :: SP (Either oldo newi) (Either oldi newo) -> 
                      SP oldi oldo -> 
                      SP newi newo
Programming with loopThroughRightSP corresponds to inheritance in object-oriented programming. The encapsulated stream processor corresponds to the inherited class. Overridden methods correspond to message constructors that the encapsulating stream processor handles itself.
Example:
Implement loopThroughRightSP using loopLeftSP together with parallel and serial compositions as appropriate.
Solution:
loopThroughRightSP ::
  SP (Either oldo i) (Either oldi o) -> SP oldi oldo ->SP i o
loopThroughRightSP spnew spold =
    loopLeftSP
      (mapSP post  -==- (spold -+- spnew)
                   -==- mapSP pre)
  where
    pre (Right input) = Right (Right input)
    pre (Left (Left newToOld)) = Left newToOld
    pre (Left (Right oldToNew)) = Right (Left oldToNew)
    post (Right (Right output)) = Right output
    post (Right (Left newToOld)) = Left (Left newToOld)
    post (Left oldToNew) = Left (Right oldToNew)
Example:
Implement serial composition using a tagged parallel composition and a loop.
Solution:
(-==-) :: SP b c -> SP a b -> SP a c
sp1 -==- sp2 =
    loopThroughRightSP (mapSP route) (sp1 -+- sp2)
  where
    route (Right a) = Left (Right a)
    route (Left (Left c)) = Right c
    route (Left (Right b)) = Left (Left b)
The combinator loopThroughBothSP,

loopThroughBothSP  ::     SP (Either l12 i1) (Either l21 o1)
                      ->  SP (Either l21 i2) (Either l12 o2)
                      ->  SP (Either i1  i2) (Either o1  o2)   
is a symmetric version of loopThroughRightSP. A composition loopThroughBothSP sp1 sp2 allows both sp1 and sp2 to communicate with the outside world and with each other (see Figure 35).

Figure 35. Circuit diagram for loopThroughBothSP.

An interesting property of loopThroughBothSP is that the circuit diagrams of the more basic combinators, -==-, -+- and loopSP, can be obtained from the circuit diagram of loopThroughBothSP by just removing wires. Other combinators are thus easy to define in terms of loopThroughBothSP.

18.3 Dynamic process creation

We implicitly made a distinction between the operators that define the dynamic behaviour of an atomic stream processors (nullSP, putSP and getSP) and the operators that are used to build static networks of stream processors (-==-, -*-, loopSP, etc.). But there is in fact no reason why networks must be static. By using combinators like -==- and -*- in a dynamic way, the number of stream processors can be made to increase dynamically. The number of stream processors can also decrease, for example if a component of a parallel composition dies (since nullSP -*- sp is equivalent to sp).

A practical application of these ideas is discussed in Section 35.4.