haskellmonadsarrowskleisli

How to use Kleisli arrows with monads?


In the Haskell Control.Arrow documentation it talks about Kleisli arrows' relationship to monads, but it is not obvious to me how to use this. I have a function which I think fits with arrows except for it involving the IO monad, so I think Kleisli arrows may help.

Take the following function which returns pairs of original and modified filenames of a directory.

import System.Directory
import System.FilePath

datedFiles target = do
    fns <- getDirectoryContents target
    tms <- mapM (fmap show . getModificationTime) fns
    return $ 
        zip fns $ 
        zipWith replaceBaseName fns $ 
        zipWith (++) (map takeBaseName fns) tms

If I had to draw it out, it would be something like this:

enter image description here

I think it can benefit from the use of Kleisli arrows, but I don't know how. Can anyone provide guidance?


Solution

  • datedFiles can be implemented using arrows because the information flows in a "fixed pipeline", as your diagram shows.

    Here's a possible implementation that does not use map or zip on lists:

    import System.Directory
    import System.FilePath
    import Control.Monad.List
    import Control.Arrow
    
    datedFiles :: FilePath -> IO [(FilePath,FilePath)]
    datedFiles = fmap runListT . runKleisli $
       (Kleisli $ ListT . getDirectoryContents) 
       >>>
       returnA &&& ((Kleisli $ liftIO . getModificationTime) >>^ show)
       >>^
       fst &&& (\(path,time) -> replaceBaseName path $ takeBaseName path ++ time)
    

    Arguably, it is not the most intuitive implementation.

    The monad for the Kleisli arrows is ListT IO, although the only nondeterminism is caused by getDirectoryContents.

    Note that the last line is a pure function; the (&&&) for the last line is using the Arrow instance for functions.

    Edit: The Wrapped typeclass from the lens package can be used to add/remove newtype wrappers a bit more succinctly. Applying it to the previous example, we end up with:

    import Control.Lens
    
    datedFiles :: FilePath -> IO [(FilePath,FilePath)]
    datedFiles = fmap runListT . runKleisli $
       ListT . getDirectoryContents ^. wrapped 
       >>>
       returnA &&& (liftIO . getModificationTime ^. wrapped >>^ show)
       >>^
       fst &&& (\(path,time) -> replaceBaseName path $ takeBaseName path ++ time)