A small program I use as my hello world of streaming libraries is the following:For each line of stdinInterpret the line as a file-nameIf the file represented by the file-name has changed... (including the possibility of being removed or added)... Then output the file-nameThis works well because you can draw a nice data-flow diagram that should convert easily to pipe-like libraries because of the data-plumbing style algorithm...However, I usually have some issues with this seemingly straight-forward exercise when it comes to splitting and re-joining a stream of values.The Machines library by Edward Kmett has been the cleanest implementation I've made so-far, but is no exception to me encountering implementation issues (hopefully just due to my misunderstandings).GistI've linked to my solution as a gist as well as appended to the end of this question as a code-block and welcome all code reviews, but I would especially like help with the following:Identifying existing functionality in the 'machines' libraries that I can remove from my implementation.Finding an exiting replacement or improvement for my splitZipWith functionRemoving the IO dependency from as many of my machines as possible{-# LANGUAGE GADTs #-}{-# LANGUAGE RankNTypes #-}{-# LANGUAGE FlexibleContexts #-}module UniqHash (main, uniqHash) whereimport Data.Machineimport Control.Monad          (when)import System.Directory       (doesFileExist)import Control.Category       (Category)import Data.Digest.Pure.MD5   (MD5Digest, hash')import Control.Monad.IO.Class (liftIO)import qualified Data.Map        as Mimport qualified Data.ByteString as B--- UniqHashmain :: IO ()main = runT_ $ stdinMachine ~> uniqHash ~> stdoutMachineuniqHash :: ProcessT IO String StringuniqHash = splitZip echo (fileExists ~> md5machine)        ~> construct (lookupM M.empty)--- HelpersstdinMachine :: SourceT IO StringstdinMachine = construct $ liftIO getContents >>= mapM_ yield . linesstdoutMachine :: ProcessT IO String ()stdoutMachine = repeatedly $ await >>= liftIO . putStrLnfileExists :: ProcessT IO String (Maybe String)fileExists = repeatedly $ do  fn <- await  ex <- liftIO $ doesFileExist fn  yield $ if ex then Just fn                else Nothingmd5machine :: ProcessT IO (Maybe String) (Maybe MD5Digest)md5machine = repeatedly $ await >>= liftIO . fileMD5 >>= yield  where  fileMD5 :: Maybe String -> IO (Maybe MD5Digest)  fileMD5 (Just fileName) = fmap (Just . hash') (B.readFile fileName)  fileMD5  Nothing        = return NothinglookupM :: (Category k, Ord o, Eq a) => M.Map o a -> PlanT (k (o, a)) o m ()lookupM m = do  (fileName, hashVal) <- await  case M.lookup fileName m of       Nothing -> yield fileName       Just v  -> when (hashVal /= v) (yield fileName)  lookupM (M.insert fileName hashVal m)-- Possibly exist in libraries?double :: MachineT IO (Is o) odouble = repeatedly $ do  i <- await  yield i  yield isplitZipWith :: ProcessT IO a a' -> ProcessT IO a b' -> TeeT IO a' b' c -> MachineT IO (Is a) csplitZipWith a b o = double ~> fit cappedT (tee a b o)  where  -- Redefined since this is private...  cappedT :: T a a b -> Is a b  cappedT R = Refl  cappedT L = ReflsplitZip :: ProcessT IO a a' -> ProcessT IO a b' -> MachineT IO (Is a) (a', b')splitZip a b = splitZipWith a b tpairtpair :: MachineT IO (T t t1) (t, t1)tpair = repeatedly $ do  fn <- awaits L  md <- awaits R  yield (fn,md)
Splitting and Joining Machines
haskell