Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use filterM with an infinite list

Tags:

haskell

monads

I was trying to find a directory name by appending a number until I found a name that doesn't yet exist:

head <$> filterM (fmap not . fexists_) [ getDatedDir t d n | n <- [0..] ]

The problem with this is that it never returns. I think the problem is that although IO is a Functor, filterM has to do all the IO before the head effects; that is, it has to evaluate fexists for every n - and that's of course infinite.

Now, I can solve this:

go t d 0
where go t d n = do
    let dir = getDatedDir t d n
    fexists_ dir >>= \case
        False -> return dir
        True  -> go t d (n+1)

But I feel that there ought to be a more elegant approach, using filterM or something similar.

This feels like a common enough pattern that there is probably an extisting function in Control.Monad, I'm just not seeing it.

like image 860
user3416536 Avatar asked Nov 13 '17 20:11

user3416536


2 Answers

There's firstM from Control.Monad.Loops:

firstM :: Monad m => (a -> m Bool) -> [a] -> m (Maybe a)

return the first value from a list, if any, satisfying the given predicate.

like image 178
pat Avatar answered Nov 15 '22 15:11

pat


An equivalent without monads, would be find :: Foldable t => (a -> Bool) -> t a -> Maybe a. But I can not find a version to let it work with monads.

An idea might be to implement findM ourselves:

import Data.Bool(bool)

findM :: Monad m => (a -> m Bool) -> [a] -> m (Maybe a)
findM f [] = return Nothing
findM f (x:xs) = f x >>= bool (findM f xs) (return (Just x))

You can then use it like:

import System.Directory(doesFileExist)

main = do
    Just fln <- findM (not . doesDirectoryExist) (map (getDatedDir t d) [0..])
    putStrLn fln

to print the first filename such that the file does not exists. Of course you can also process fln in a different way.

like image 2
Willem Van Onsem Avatar answered Nov 15 '22 15:11

Willem Van Onsem