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?
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.
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.
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!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With