[left-fold operator for enumerating the lines of a text file]
..
enumLines :: (a -> String -> Either a a) -> a -> FilePath -> IO a
enumLines iter accum filename = do
  h <- openFile filename ReadMode
  flip fix accum $
    \iterate accum -> do
      try_line <- try (hGetLine h)
      case try_line of
        Left e -> hClose h >> return accum
        Right line -> do
          case iter accum line of
            Left accum -> hClose h >> return accum
            Right accum -> iterate accum
..
getHeaders :: S.Set String -> FilePath -> IO (S.Set String, M.Map String String)
getHeaders hdrs file = enumLines findHdrs (hdrs,M.empty) file where

we can keep the Left/Right implicit, using either:

   getHeaders1 hdrs file = enumLines findHdrs (hdrs,M.empty) file
     where
     enumLines iter accum filename = do
       h <- openFile filename ReadMode
flip fix accum $ \iterate accum -> join $ (fmap (either
                          (const $ hClose h >> return accum)
                          (either ((hClose h >>) . return) iterate . iter 
accum)))
                        (try (hGetLine h))

or extract the reusable loop-with-exit-by-either functionality:

   loopME m stop continue acc = m >>= either (stop acc) (continue (loopME m 
stop continue) acc)

   getHeaders2 hdrs file = enumLines findHdrs (hdrs,M.empty) file
     where
     enumLines iter accum f = do
       h <- openFile f ReadMode
       loopME (try (hGetLine h))
              (\acc left->hClose h >> return acc)
              (\loop acc right->either ((hClose h >>) . return) loop (iter acc 
right))
              accum

or sneak some lazy i/o back in, using a fold-with-exit-by-either, similar to 
loopME:

   withFile       path m = bracket (openFile path ReadMode) hClose m
   withContentsOf path f = withFile path ((((return $!) . f ) =<<) . 
hGetContents)
   withLinesOf    path f = withContentsOf path (f . lines)

   foldE f a []     = a
   foldE f a (x:xs) = either id (\a'->foldE f a' xs) (f a x)

   getHeaders3 hdrs file = withLinesOf file (foldE findHdrs (hdrs,M.empty))

To use this, you provide an "iteratee", a function which takes an
accumulator and a line from the file, and returns a new accumulator
embedded in an Either. Using the Left branch causes immediate
termination of the enumeration. For example, to search for the first
occurrence of each of a set of email headers:

  findHdrs accum@(wanted,found) line =
    if null line
      then Left accum
      else
        case headerLine line of
          Nothing -> Right accum
          Just hdr ->
            case findDelete hdr wanted of
              Nothing -> Right accum
              Just wanted ->
                let accum = (wanted, M.insert hdr line found) in
                  if S.null wanted
                    then Left accum
                    else Right accum

headerLine :: String -> Maybe String
headerLine (':':xs) = Just []
headerLine (x:xs) = fmap (x:) (headerLine xs)
headerLine [] = Nothing

findDelete :: Ord a => a -> S.Set a -> Maybe (S.Set a)
findDelete e s = if S.member e s
  then Just (S.delete e s)
  else Nothing

It's a bit of a case-analysis nightmare

indeed, this part can be cleaned up considerably, using Monad Maybe:

   findHdrs accum@(wanted,found) line =
     if null line || S.null wanted
       then Left accum
else maybe (Right accum) id $ do (field,value) <- headerLine line wanted' <- findDelete field wanted
               let found' = M.insert field value found
               return $! (Right $! ((,) $! wanted') $! found')

   headerLine :: String -> Maybe (String,String)
headerLine xs = do (field,':':value) <- return (span (/=':') xs) let value' = dropWhile isSpace value
                      return $! ((,) $! field) $! strictly value'

   findDelete :: Ord a => a -> S.Set a -> Maybe (S.Set a)
   findDelete e s = guard (S.member e s) >> return (S.delete e s)

   strictly l = length l `seq` l

running the three variants over a moderately sized directory (>3k emails, one including a hugs-tarball;-), 1/2 are roughly equivalent, but Hugs claims that 3 allocates less and needs fewer garbage collections than 1/2, while GHC claims that it is the other way round..

claus

_______________________________________________
Haskell-Cafe mailing list
[email protected]
http://www.haskell.org/mailman/listinfo/haskell-cafe

Reply via email to