haskellhaskell-turtle

Haskell Turtle - split a shell


Is it possible to split a Shell in Turtle library (Haskell) and do different things to either split of the shell, such that the original Shell is only run once ?

             /---- shell2
---Shell1 --/
            \
             \-----shell3

For instance, how to do

do
  let lstmp = lstree "/tmp"
  view lstmp
  view $ do
    path <- lstmp
    x <- liftIO $ testdir path
    return x

such that lstree "/tmp" would only run once.

Specifically I would like to send Shell 2 and Shell 3 to different files using output.


Solution

  • You won't be able to split a Shell into two separate shells that run simultaneously, unless there's some magic I don't know. But file writing is a fold over the contents of a shell or some other succession of things. It is built into turtle that you can always combine many folds and make them run simultaneously using the Control.Foldl material - here

    foldIO ::  Shell a -> FoldM IO a r -> IO r  -- specializing
    

    A shell is secretly a FoldM IO a r -> IO r under the hood anyway, so this is basically runShell. To do this we need to get the right Shell and the right combined FoldM IO. The whole idea of the Fold a b and FoldM m a b types from the foldl package is simultaneous folding.

    I think the easiest way to get the right shell is just to make the lstree fold return a FilePath together with the result of testdir. You basically wrote this:

    withDirInfo :: FilePath -> Shell (Bool, FilePath)
    withDirInfo tmp = do
        let lstmp = lstree tmp
        path <- lstmp
        bool <- liftIO $ testdir path
        return (bool, path)
    

    So now we can get a Shell (Bool, FilePath) from /tmp This has all the information our two folds will need, and thus that our combined fold will need.

    Next we might write a helper fold that prints the Text component of the FilePath to a given handle:

    sinkFilePaths :: Handle -> FoldM IO FilePath ()
    sinkFilePaths handle = L.sink (T.hPutStrLn handle . format fp)
    

    Then we can use this Handle -> FoldM IO FilePath () to define two FoldM IO (Bool, FilePath) (). Each will write different stuff to different handles, and we can unite them into a single simultaneous fold with <*. This is an independent FoldM IO ... and can be applied e.g. to a pure list of type [(Bool, FilePath)] using L.fold and it will write different things from the list to the different handles. In our case, though, we will apply it to the Shell (Bool, FilePath) we defined.

    The only subtle part of this is the use of L.handlesM to print only the second element, in both cases, and only those filtered as directories in the other. This uses the _2 lens and filtered from the lens libraries. This could probably be simplified, but see what you think:

    {-#LANGUAGE OverloadedStrings #-}
    import Turtle
    import qualified Control.Foldl as L
    import qualified System.IO as IO
    import Control.Lens (_2,filtered)
    import qualified Data.Text.IO as T
    
    main = IO.withFile "tmpfiles.txt" IO.WriteMode $ \h ->
           IO.withFile "tmpdirs.txt" IO.WriteMode $ \h' -> do
            foldIO (withDirInfo "/tmp") (sinkFilesDirs h h') 
    
    withDirInfo :: Turtle.FilePath -> Shell (Bool, Turtle.FilePath)
    withDirInfo tmp = do
        let lstmp = lstree tmp
        path <- lstmp
        bool <- liftIO $ testdir path
        return (bool, path)
    
    sinkFilePaths :: Handle -> FoldM IO Turtle.FilePath ()
    sinkFilePaths handle = L.sink (T.hPutStrLn handle . format fp)
    
    sinkFilesDirs  :: Handle -> Handle -> FoldM IO (Bool, Turtle.FilePath) ()
    sinkFilesDirs h h' = allfiles <* alldirs where
    
      allfiles :: L.FoldM IO (Bool, Turtle.FilePath) ()
      allfiles = L.handlesM _2 (sinkFilePaths h)
      -- handle the second element of pairs with sinkFilePaths
    
      alldirs :: FoldM IO (Bool, Turtle.FilePath) ()
      alldirs = L.handlesM (filtered (\(bool,file) -> bool) . _2) (sinkFilePaths h')
     -- handle the second element of pairs where the first element
     -- is true using sinkFilePaths