Skip to content

Instantly share code, notes, and snippets.

@nkpart
Last active September 10, 2020 04:47
Show Gist options
  • Save nkpart/fa3d2d7a482e6b3904fa to your computer and use it in GitHub Desktop.
Save nkpart/fa3d2d7a482e6b3904fa to your computer and use it in GitHub Desktop.
fmap . fmap . fmap

fmap . fmap . fmap

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/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment