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
fmap
over 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/