7

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?

AndrewC
  • 32,300
  • 7
  • 79
  • 115
user313967
  • 2,013
  • 2
  • 14
  • 9

3 Answers3

23
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.

dave4420
  • 46,404
  • 6
  • 118
  • 152
4

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.

Jan Christiansen
  • 3,153
  • 1
  • 19
  • 16
2

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!

Chris Taylor
  • 46,912
  • 15
  • 110
  • 154
  • By the way, are there any statics how long it takes until a question regarding Haskell is answered? I am under the impression that 90 percent of all Haskell questions are answered nearly immediately. While this does not say anything about the quality of the answers, in my opinion they have a quite high quality as well. – Jan Christiansen Sep 27 '12 at 11:19
  • 2
    @JanChristiansen: You could answer that using the stack exchange data explorer if you want. I did a very rough approximation (filter obvious outliers, ignore self-answers, &c.), and the typical (i.e., median) time was roughly 20 minutes until the first answer is posted. – C. A. McCann Sep 27 '12 at 14:28