Haskell: Hiding failures in lazy IO

188 views Asked by At

This is a noob question.

I'd like to write a function which provides a lazy stream of images, presumably something like:

imageStream :: [IO Image]

Unfortunately, the function which reads images can fail, so it looks like:

readImage :: IO (Maybe Image)

So, the function I can write looks like:

maybeImageStream :: [IO (Maybe Image)]

How do I implement a function such as the following, while still keeping lazy IO?

flattenImageStream :: [IO (Maybe Image)] -> [IO Image]

Semantically, when you ask flattenImageStream for the next image, it should iterate through the list and attempt to read each image. It does this until it finds an image that loads, and returns it.

EDIT: There seems to be some disagreement in the answers. Some have suggested solutions that use sequence, but I'm pretty sure I tested that and found it destroys laziness. (I'll test it again to be sure when I get back to my computer.) Someone also suggested using unsafeInterleaveIO. From the documentation for that function, it seems it would work, but obviously I want to respect the type system as much as possible.

4

There are 4 answers

1
Gabriella Gonzalez On BEST ANSWER

You can use ListT from pipes, which provides a safer alternative to lazy IO that does the right thing in this case.

The way you model your lazy stream of potentially failing images is:

imageStream :: ListT IO (Maybe Image)

Assuming that you had some image loading function of type:

loadImage :: FileName -> IO (Maybe Image)

.. then the way you build such a stream would be something like:

imageStream = do
    fileName <- Select $ each ["file1.jpg", "file2.jpg", "file3.jpg"]
    lift $ loadImage fileName

If you use the dirstream library, then you can even lazily stream over the directory contents, too.

The function that filters out only the successful results would have this type:

flattenImageStream :: (Monad m) => ListT m (Maybe a) -> ListT m a
flattenImageStream stream = do
    ma <- stream
    case ma of
        Just a  -> return a
        Nothing -> mzero

Notice that this function works for any base monad, m. There is nothing IO-specific about it. It also preserves laziness!

Applying flattenImage to imageStream, gives us something of type:

finalStream :: List IO Image
finalStream = flattenImage imageStream

Now let's say that you have some function that consumes these images, of type:

useImage :: Image -> IO ()

If you want to process the final ListT using the useImage function, you just write:

main = runEffect $
    for (every finalStream) $ \image -> do
        lift $ useImage image

That will then lazily consume the image stream.

Of course, you could also play code golf and combine all of that into the following much shorter version:

main = runEffect $ for (every image) (lift . useImage)
  where
    image = do
        fileName   <- Select $ each ["file1.jpg", "file2.jpg", "file3.jpg"]
        maybeImage <- lift $ loadImage fileName           
        case maybeImage of
            Just img -> return img
            Nothing  -> mzero

I'm also thinking of adding a fail definition for ListT so that you could just write:

main = runEffect $ for (every image) (lift . useImage)
  where
    image = do
        fileName <- Select $ each ["file1.jpg", "file2.jpg", "file3.jpg"]
        Just img <- lift $ loadImage fileName           
        return img
1
jwodder On

Implementing this as requested seems like it would require knowing outside of the IO monad whether a value inside IO was Nothing, and as IO is designed to prevent its values from "leaking out" into the outside purely functional world (unsafePerformIO notwithstanding), this would be impossible. Instead, I recommend producing an IO [Image]: use sequence to convert the [IO (Maybe Image)] to IO [Maybe Image], and then use Data.Maybe.catMaybes within the IO monad (e.g., with fmap or liftM) to convert to IO [Image], e.g.:

flattenImageStream = fmap catMaybes $ sequence maybeImageStream
0
Arjan On

as suggested u can turn [m a] into m [a] using sequence

so you get:

imageStream :: IO [Image]

then you can use cayMaybes from Data.Maybe to keep just the Just values:

catMaybes `liftM` imageStream
3
DiegoNolan On

I don't think any of these other answers are doing exactly what you want. Because i'm pretty sure catMaybes will just skip over the image and not try to reload it. If you want to just keep trying to reload an image try this.

flattenImageStream :: [IO (Maybe Image)] -> IO [Image]
flattenImageStream xs = mapM untilSuc xs

untilSuc :: IO (Maybe a) -> IO a
untilSuc f = do
   res <- f
   case res of
      Nothing -> untilSuc f
      Just i  -> return i 

But what you are doing is kind of strange. What if you have the wrong file path? What if the image simply can't be loaded? You'll just try to load an image forever. You should probably have a number of times to try and load the image before it gives up.