Functors and Traversables compose.
You might have seen this statement before. This is how you can take advantage of it.
Composing many fmap calls gives you a function that will drill down into a structure and modify it at a certain depth in that nested structure.
For example:
>>> let x = [("hello", [1,2]),("world", [3,4::Float])]
>>> :t x
x :: [(String, [Float])]
We have 3 layers of Functors in our data:
- The outer list
- The pair, where we can
fmapover the right hand side - The list on the right of the pair
Now we can modify the Floats by composing fmap 3 times to get down to them:
>>> :{
| (fmap . -- step down into the first list
| fmap . -- now over the right hand side of the pair (the functor on tuples maps this side of it)
| fmap) -- and then into the list of numbers
| (* 1000) -- the function to apply at the bottom
| x -- our structure
|:}
[("hello",[1000,2000]),("world",[3000,4000])]
>>> -- Without the commentary, it looks like this:
>>> (fmap . fmap . fmap) (* 1000) x
[("hello",[1000,2000]),("world",[3000,4000])]
If your change requires effects, use traverse . traverse . traverse . etc. where you would normally apply one traverse:
>>> import System.Random
>>> import Data.Traversable
>>> let addExtremeR v = fmap (\n -> v - log (- log n)) (randomRIO (0,1::Float))
>>> :t addExtremeR
addExtremeR :: Float -> IO Float
>>> (traverse . traverse . traverse) addExtremeR x
[("hello",[-0.3721832,2.8857899]),("world",[6.078006,3.832755])]
In general, if we nest 2 Functors, we have another Functor. If we nest 2 Traversables, we have another Traversable.
The transformers package includes a data type, Compose that has a Functor/Traversable instance if the underlying nested structure has them for each of its components: https://hackage.haskell.org/package/transformers-0.4.2.0/docs/Data-Functor-Compose.html
To use it, we need to wrap our type up in Compose a sufficient number of times (once for each extra fmap/traverse) and then unwrap again at the end.
>>> import Data.Functor.Compose
>>> getCompose . getCompose $ fmap succ ((Compose . Compose) $ x
[("hello",[2,3]),("world",[4,5])]
>>> fmap (getCompose . getCompose) $ traverse addExtremeR ((Compose . Compose) $ x)
[("hello",[-0.17058408,0.77387905]),("world",[4.821744,7.925271])]
This is not the end of the story either. There are a number of other typeclasses that compose when they are nested, including: Applicative, Foldable, Apply, Alternative, Zip, Unzip, Foldable1, Traversable1, Bifoldable, BiTraversable, Bifoldable1, and BiTraversable1.
Here's a Foldable example, where we sum those same Floats:
>>> (foldMap . foldMap . foldMap) Sum x
Sum {getSum 10.0}
Composition rocks! Take advantage of it and know that when you see a few fmaps lying in a row, it's just targetting a particular level in some data.
\m/