19 Application programming with plain stream processors

Although plain stream processors are mostly used in conjunction with fudgets, they can be used independently. In this chapter, we take a look at some examples of interactive Haskell programs written using stream processors.

19.1 An adding machine

In Section 16.3 we defined

sumSP :: Int -> SP Int Int
that computes the accumulating sum of a stream of integers. Let us write a complete Haskell program that uses sumSP to implement a simple adding machine.

Haskell provides the function interact, which allows functions of type [Char] -> [Char] to be used as programs (as in Landin's stream I/O model outlined in Chapter 4). By combining this with the function runSP,

runSP :: SP i o -> [i] -> [o]
(from Section 16.1) we can run stream processors of type SP Char Char:

main = interact (runSP mainSP)
mainSP :: SP Char Char
mainSP = ...
To be able to use sumSP we need only add some glue functions that convert the input stream of characters to a stream of numbers and conversely for the output stream. This is done in two stages. First, the stream-processor equivalents of the standard list functions lines and unlines are used to process input and output line by line, instead of character by character:

mainSP = unlinesSP -==- adderSP -==- linesSP
adderSP :: SP String String
adderSP = ...
Now the standard functions show and read are used to convert between strings and numbers,

adderSP = mapSP show -==- sumSP 0 -==- mapSP read
and the program is complete.
Example:
Implement unlinesSP :: SP String Char.
Solution:
unlinesSP = concatMapSP (\s -> s++"\n")
Example:
Implement linesSP :: SP Char String
Solution:
linesSP = lnSP []
  where
    lnSP acc =
      getSP $ \msg ->
      case msg of
        '\n'  -> putSP (reverse acc) (lnSP [])
        c     -> lnSP (c : acc)

19.2 A stream processor for input line editing

In the example above, it was assumed that input is line buffered (cooked terminal mode in Unix), i.e., the system allows the user to enter a line of text and edit it by using the backspace key, (and possibly other cursor motion keys) and send it to the program by pressing the Return key. The system is thus responsible for echoing characters typed on the keyboard, to the screen (Figure 36).

Figure 36. Line buffered input.

Assuming a simpler system, where keyboard input is fed directly to the program, and the only characters shown on the screen are those output by the program (raw terminal mode in Unix) (Figure 37), the stream-processor combinator lineBufferSP is now defined to do the job:

Figure 37. Unbuffered input.

lineBufferSP :: SP String Char -> SP Char Char
It takes a stream processor that expects the input to be line buffered, and returns a stream processor that does the necessary processing of the input: buffering, echoing, etc., so that it can work in an unbuffered environment.

We implement lineBufferSP using loopThroughRightSP:

lineBufferSP progsp = loopThroughRightSP bufSP progsp
  where
    bufSP :: SP (Either Char Char) (Either String Char)
    bufSP = ...
We get the connectivity shown in Figure 38, i.e., bufSP will receive program output and keyboard input on its input stream and should produce input lines and screen output on its output stream.

Figure 38. Circuit diagram for lineBufferSP.

The implementation of bufSP is shown in Figure 39.

bufSP = inputSP ""

inputSP line = getSP $ either fromProgsp fromKeyboard
  where
    fromProgsp c = putSP (toScreen c) (inputSP line)

    fromKeyboard c =
      case c of
        -- The Enter key:
        '\n' -> putSP (toScreen '\n') $
                putSP (toProgsp (reverse line)) $
                bufSP
        -- The backspace key:
        '\b' -> if null line
                then inputSP line
                else putsSP (map toScreen "\b \b") $
                     inputSP (tail line)
        -- Printable characters:
        _    -> putSP (toScreen c) $
                inputSP (c:line)

    toScreen = Right
    toProgsp = Left
    

Figure 39. bufSP - the core of lineBufferSP.

Using lineBufferSP, the adding machine in the previous section can be adapted to run in raw terminal mode by change mainSP to:

mainSP = lineBufferSP (unlinesSP -==- adderSP)

19.3 Running two programs in parallel on a split screen

This last example is a combinator that splits the terminal screen into two windows and runs two programs in parallel, one in each window:

splitViewSP :: SP Char Char -> SP Char Char -> SP Char Char
A simple implementation of splitViewSP can be structured as follows:

splitViewSP sp1 sp2 =
        mergeSP -==- (sp1 -+- sp2) -==- distrSP
  where
        distrSP :: SP Char (Either Char Char)
        distrSP = ...
        mergeSP :: SP (Either Char Char) Char
        mergeSP = ...
distrSP takes the keyboard input and sends it to one of the two windows. The user can switch windows by pressing a designated key.

mergeSP takes the two output streams from the windows and produces a merged stream, which contains the appropriate cursor control sequences to make the text appear in the right places on the screen. This can be done in different ways depending on the terminal characteristics. A simple solution, if scrolling is not required, is to split the processing into two steps: the first being to interpret the output streams from the two windows individually to keep track of the current cursor position using a stream processor like

trackCursorSP :: SP Char ((Int,Int),Char)
It takes a character stream containing a mixture of printable characters and cursor control characters, and produces a stream with pairs of cursor positions and printable characters. The next step is to merge the two streams and feed them into a stream processor that generates the appropriate cursor motion commands for the terminal:

encodeCursorMotionSP :: SP ((Int,Int),Char) Char
Thus we have

mergeSP =
        encodeCursorMotionSP -==-
        mapSP stripEither -==-
        (trackCursorSP -+- trackCursorSP)
Using the above outlined implementation of mergeSP, we get the circuit diagram shown in Figure 40 for splitViewSP sp1 sp2:

Figure 40. Circuit diagram for splitViewSP sp1 sp2.

Filling in some details we ignored in the above description, we get the implementation shown in Figure 41.

splitViewSP :: (Int,Int) -> SP Char Char -> SP Char Char -> SP Char Char
splitViewSP (w,h) sp1 sp2 =
  mergeSP -==- (sp1 -+- sp2) -==- distrSP Left Right
where
  mergeSP = encodeCursorMotionSP -==-
            mapSP stripEither -==-
                (trackCursorSP (w,h1) -+-
                 (mapSP movey -==- trackCursorSP (w,h2)))

  h1 = (h-1) `div` 2
  h2 = h-1-h1

  movey ((x,y),c) = ((x,y+h1+1),c)

  distrSP dst1 dst2 =
    getSP $ \ c ->
    case c of
      '\t' -> distrSP dst2 dst1
      _    -> putSP (dst1 c) $ distrSP dst1 dst2

trackCursorSP :: (Int,Int) -> SP Char ((Int,Int),Char)
trackCursorSP size = mapstateSP winpos (0,0)
  where winpos p c = (nextpos p c,[(p,c)])

encodeCursorMotionSP :: SP ((Int,Int),Char) Char
encodeCursorMotionSP = mapstateSP term (-1,-1)
  where
    term cur@(curx,cury) (p@(x,y),c) =
        (nextpos p c,move++[c])
      where
        move = if p==cur
               then ""
               else moveTo p

nextpos :: (Int,Int) -> Char -> (Int,Int)
nextpos p c = ... -- cursor position after c has been printed

moveTo :: (Int,Int) -> String
moveTo (x,y) = ... -- generate the appropriate cursor control sequence

Figure 41. An implementation of splitViewSP.