Search code examples
haskellhaskell-lenslenses

How can I express `mapM` with `concat` using Lenses to concatenate results of an IO operation?


I'm trying to figure out a way how to combine traverseOf with >>= in such a way that would allow the following.

TLDR; A simple example in plain Haskell would be something like this, but using lenses deep inside a data structure.

λ> fmap concat $ mapM ((return :: a -> IO a) . const ["he", "he"]) ["foo", "bar", "baz"]
["he","he","he","he","he","he"]

Here's a lengthy explanation with examples

data Foo = Foo [Bar] deriving Show
data Bar = Baz | Qux Int [String] deriving Show

makePrisms ''Foo
makePrisms ''Bar

items :: [Foo]
items = [Foo [Baz], Foo [Qux 1 ["hello", "world"], Baz]]

-- Simple replacement with a constant value
constReplace :: [Foo]
constReplace = over (traverse._Foo.traverse._Qux._2.traverse) (const "hehe") items
-- λ> constReplace
-- [Foo [Baz],Foo [Qux 1 ["hehe","hehe"],Baz]]

-- Doing IO in order to fetch the new value. This could be replacing file names
-- with the String contents of the files.
ioReplace :: IO [Foo]
ioReplace = (traverse._Foo.traverse._Qux._2.traverse) (return . const "hehe") items
-- λ> ioReplace
-- [Foo [Baz],Foo [Qux 1 ["hehe","hehe"],Baz]]

-- Replacing a single value with a list and concatenating the results via bind
concatReplace :: [Foo]
concatReplace = over (traverse._Foo.traverse._Qux._2) (>>= const ["he", "he"]) items
-- λ> concatReplace
-- [Foo [Baz],Foo [Qux 1 ["he","he","he","he"],Baz]]

-- Same as the previous example, but the list comes from an IO action
concatIoReplace :: IO [Foo]
concatIoReplace = (traverse._Foo.traverse._Qux._2) (return . (>>= const ["he", "he"])) items
-- λ> concatIoReplace
-- [Foo [Baz],Foo [Qux 1 ["he","he","he","he"],Baz]]

Now the last example is where the problem is, because I've cheated a little bit by changing around the function that's being applied. In the concatReplace I was able to use >>= (thanks to the helpful guys on #haskell-lens channel) to implement the concatMap-like functionality. But in my real code the function I have is String -> IO [String], which would look something like this

correctConcatIo :: IO [Foo]
correctConcatIo = (traverse._Foo.traverse._Qux._2) (>>= (return . const ["he", "he"])) items

But this example doesn't typecheck anymore. What I need is to basically put together the logic from ioReplace and concatReplace in a way that I would be able to apply a function with the type String -> IO [String] to a data structure containing [String].


Solution

  • You can only replace a String with [String] if it's already in a list (consider trying to stick a [Int] back into _Qux._1), so you have to turn your function into [String]->IO [String] and replace the whole list, using some approach like you've already demonstrated:

    concatMapM f l = fmap concat (mapM f l)
    
    doIOStuff s = return ['a':s, 'b':s]
    
    concatIO :: IO [Foo]
    concatIO = (traverse._Foo.traverse._Qux._2) (concatMapM doIOStuff) items
    

    You can even compose that concatMapM onto the end to get something with a LensLike type, but it's not flexible enough to use with most of the lens combinators.