Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to map a list of functions over multiple arguments in Haskell?

I have three functions (getRow,getColumn,getBlock) with two arguments (x and y) that each produce a list of the same type. I want to write a fourth function that concatenates their outputs:

outputList :: Int -> Int -> [Maybe Int]
outputList x y = concat . map ($ y) $ map ($ x) [getRow,getColumn,getBlock]

The function works, but is there a way to rewrite the double map (with three '$'s) to a single map?

like image 883
user313967 Avatar asked Sep 27 '12 10:09

user313967


3 Answers

import Data.Monoid

outputList :: Int -> Int -> [Maybe Int]
outputList = mconcat [getRow, getColumn, getBlock]

You deserve an explanation.

First, I'll explicitly note that all these functions have the same type.

outputList, getRow, getColumn, getBlock :: Int -> Int -> [Maybe Int]

Now let's start with your original definition.

outputList x y = concat . map ($ y) $ map ($ x) [getRow,getColumn,getBlock]

These functions result in a [Maybe Int], and a list of anything is a monoid. Monoidally combining lists is the same as concatenating the lists, so we can replace concat with mconcat.

outputList x y = mconcat . map ($ y) $ map ($ x) [getRow,getColumn,getBlock]

Another thing that's a monoid is a function, if its result is a monoid. That is, if b is a monoid, then a -> b is a monoid as well. Monoidally combining functions is the same as calling the functions with the same parameter, then monoidally combining the results.

So we can simplify to

outputList x = mconcat $ map ($ x) [getRow,getColumn,getBlock]

And then again to

outputList = mconcat [getRow,getColumn,getBlock]

We're done!


The Typeclassopedia has a section about monoids, although in this case I'm not sure it adds that much beyond the documentation for Data.Monoid.

like image 164
dave4420 Avatar answered Jan 04 '23 13:01

dave4420


As a first step we observe that your definition

outputList x y = concat . map ($ y) $ map ($ x) [getRow,getColumn,getBlock]

can be rewritten using the function composition operator (.) instead of the function application operator ($) as follows.

outputList x y = (concat . map ($ y) . map ($ x)) [getRow,getColumn,getBlock]

Next we notice that map is another name for fmap on lists and satisfies the fmap laws, therefore, in particular, we have map (f . g) == map f . map g. We apply this law to define a version using a single application of map.

outputList x y = (concat . map (($ y) . ($ x))) [getRow,getColumn,getBlock]

As a final step we can replace the composition of concat and map by concatMap.

outputList x y = concatMap (($ y) . ($ x)) [getRow,getColumn,getBlock]

Finally, in my opinion, although Haskell programmers tend to use many fancy operators, it is not a shame to define the function by

 outputList x y = concatMap (\f -> f x y) [getRow,getColumn,getBlock]

as it clearly expresses, what the function does. However, using type class abstractions (as demonstrated in the other answer) can be a good thing as you might observe that your problem has a certain abstract structure and gain new insights.

like image 33
Jan Christiansen Avatar answered Jan 04 '23 11:01

Jan Christiansen


I would go with @dave4420's answer, as it's most concise and expresses exactly what you mean. However, if you didn't want to rely on Data.Monoid then you could rewrite as follows

Original code:

outputList x y = concat . map ($ y) $ map ($ x) [getRow,getColumn,getBlock]

Fuse the two maps:

outputList x y = concat . map (($y) . ($x)) [getRow,getColumn,getBlock]

Replace concat . map with concatMap:

outputList x y = concatMap (($y) . ($x)) [getRow,getColumn,getBlock]

And you're done.

Edit: aaaaand this is exactly the same as @Jan Christiansen's answer. Oh well!

like image 40
Chris Taylor Avatar answered Jan 04 '23 12:01

Chris Taylor