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:
I think it can benefit from the use of Kleisli arrows, but I don't know how. Can anyone provide guidance?
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)