ListT proposal for `transformers`

Gabriel Gonzalez gabriel439 at
Sun Jul 14 16:37:21 CEST 2013

On 07/14/2013 03:44 AM, Ben Millwood wrote:
> On Sat, Jul 13, 2013 at 08:47:02PM -0700, Gabriel Gonzalez wrote:
>> I've been working on a "ListT done right" for submission into 
>> `transformers`.  I could split it off into its own package, but I 
>> would like to first run it by you all, particularly Ross, to see if 
>> this can be included this in `transformers` because I feel that 
>> `transformers` is where this belongs.
> I'm not sure. I'm inclined to think that the reason that ListT's 
> violation of the monad laws upsets no-one is because no-one actually 
> uses it. I don't think we should include the transformer just for the 
> sake of having one.

This is a chicken-and-egg problem.  Nobody uses it because it violates 
the laws and there is no good `ListT` implementation on Hackage.

Also, I have two packages I want to release that do use `ListT`, which 
is the reason I am proposing this.

> I've found myself using something like the new ListT before, but it's 
> a bit awkward to have two new datatypes, and difficult to use existing 
> list machinery with it, so I'm not sure it really pays off as an 
> abstraction.
> Do we have compelling use cases for either the old or the new ListT? 
> Is the old ListT used anywhere on Hackage? In lieu of these things, I 
> might propose just removing it altogether.

Here are some use cases I came up with that motivated me to fix this in 
the first place.

First, a back-tracking effectful parser:

     -- 's' is the unconsumed input, 'm' is the base monad, 'r' is the 
parsed value
     newtype ParseT s m r = ParseT { unParseT :: StateT s (ListT m) r }
         deriving (Functor, Applicative, Monad, MonadPlus)

     instance MonadTrans (ParseT s) where
         lift = ParseT . lift . lift

This is the effectful generalization of the backtracking Hutton-Meijer 
parser, typically define as:

     type ParseT s r = StateT s [] r

... except that the `ListT` version is a monad transformer so you can 
interleave effects.  I use this to print debugging information while 
parsing (whenever `parsec` and `attoparsec` error messages are not 
sufficiently helpful).

Another case is traversing a directory tree.  You can see example code 
I've been writing up that traverses directory trees using `ListT` here:

You use it like this:

     recurse :: FilePath -> ListT SafeIO FilePath
     recurse path = do
         child <- contents path
         isVis <- visible child
         guard isVis
         isDir <- directory child
         return child <|> (guard isDir >> recurse child)

This version is lazy and traverses the minimal number of directories to 
provide the number of demanded results.  The old `ListT` would traverse 
the entire directory tree before providing even the first result.

Edward raised another important point, which is how do you easily read 
out the result.  In the case where you want to consume all the results 
you can use the `foreach` combinator, which I included in the code:

     foreach :: (Monad m) => ListT m a -> (a -> m b) -> m ()

In the case where you do not want to demand the result, you can convert 
the `ListT` to a `Producer` from `pipes`, using `fromListT`:

     fromListT :: (Monad m) => ListT m a -> Producer a m ()

Then you can use `pipes` combinators like `take` to only demand the 
first few elements of the list:

     -- Note that this is using the `pipes-4.0.0` API on Github
     import Pipes
     import qualified Pipes.Prelude as P

     exampleListT :: ListT IO String

     exampleProducer :: () -> Producer String IO ()
     exampleProducer () = fromListT exampleListT

     main = runEffect $ (exampleProducer >-> P.take >-> P.stdout) ()

In fact, `ListT` is quite a nice fit for `pipes` because the `ListT` 
monad has an exact correspondence with one of the `pipes` categories 
(specifically the "respond" category):

     fromListT . (f >=> g) = (fromListT . f) />/ (fromListT . g)

     fromListT . return = respond

Right now I have a `ListT` implementation in the `pipes` package, but I 
got several requests to move it into `transformers` because people felt 
that `ListT` should be even lower in the library hierarchy than 
`pipes`.  I don't mind providing `ListT` in `pipes`, but it seems odd to 
tell people to use `pipes` for something as simple as `ListT`

There is another important question that Edward didn't mention, but that 
is equally important: How do you easily build `ListT` computations?  
Again, `pipes` makes this very easy:

     toListT :: (Monad m) => Producer a m () -> ListT m a

     -- toListT . fromListT = id
     -- fromListT . toListT = id

This allows people to assemble the `ListT` computation monadically as a 
`Producer`, then package it up as a `ListT` when they are done.  Here's 
an example:

     exampleListT2 :: ListT IO Int
     exampleListT2 = toListT $ forM_ [1..3] $ \i -> do
         lift $ putStrLn $ "Selecting: " ++ show i
         respond i

That creates a `ListT` computation that branches three times, printing 
the branch it choose before taking that branch.  Then you can assemble 
these in the `ListT` monad:

     total :: ListT IO (Int, Int)
     total = do
         x <- exampleListT2
         y <- exampleListT2
         return (x, y)

Here is some example output if that description is unclear:

 >>> main = foreach total print
     Selecting: 1
     Selecting: 1
     Selecting: 2
     Selecting: 3
     Selecting: 2
     Selecting: 1
     Selecting: 2
     Selecting: 3
     Selecting: 3
     Selecting: 1
     Selecting: 2
     Selecting: 3

Anyway, I can certainly provide this in `pipes`, but I just wanted to 
give `transformers` a try first to see if you all were interested.  I 
think many creative applications of `ListT` have been stymied simply 
because there is no high-quality `ListT` application on Hackage, but if 
`pipes` has to be the hiqh-quality `ListT` implementation then I am fine 
with that, too.

More information about the Libraries mailing list