Recently, a a good friend of mine has been explaining to me how powerful the traverse
function is in Haskell. The main point being that it's a generalization of both map
and fold
.
For those who are unfamiliar, traverse
has the following function definition:
traverse
:: (Applicative f, Traversable t) => (a -> f b) -> t a -> f (t b)
At first glance, it was very unclear to me how one could obtain the features of fold
within traverse. Things only started to click after I digested the following line:
To get fold, choose an applicative that “accumulates” the result you want
In other words, we can get fold
for free simply by choosing our desired applicative to wrap b
in.
Lets say we have a list of numbers, and we would like to ensure that all the numbers in the list are all Just
even numbers, or Nothing
.
Without using traverse
could do it like so:
myData = [1,2,3,4,5]
isEven x = x `rem` 2 == 0
myResult = do
let filtered = filter isEven myData
if length filtered == length myData
then Just filtered
else Nothing
It works, however we can make it more succinct simply by using traverse
.
myData = [2,4,6,8]
isEvenF x
| x `rem` 2 == 0 = Just x
| otherwise = Nothing
myResult = traverse isEvenF myData
While the previous example was nice and short, it fails to showcase where (in my opinion) traverse
excels at - seperating data handling logic from the data traversing logic. This is particularly useful in highly nested data structures.
What do I mean by that? Lets say we have a list of numbers. And we would like to sum all the even numbers within the list. Traditionally, we can do it like so:
myData = [1,2,3,4,5]
isEven x = x `rem` 2 == 0
myResult = sum $ filter isEven myData
But what if we had a nested list?
myNestedData = take 6 (repeat myData)
myNestedResult = do
let temp1 = filter isEven <$> myNestedData
let temp2 = sum <$> temp1
sum temp2
I'm sure we can refactor this to make it look neater, but things do get a bit messy as our data structures gets more nested / complicated.
Compare that with the traverse
approach:
import Control.Monad.State.Lazy
myData = [1,2,3,4,5]
addIfEven x
| x `rem` 2 == 0 = (+x)
| otherwise = id
sumIfEven x = do
modify (addIfEven x)
return x
myResult = execState (traverse sumIfEven myData) 0
While the initial setup is messier, we can easily compose traverse
so we can easily perform the same operation regardless of how nested the underlying data structure is:
myNestedData = take 6 (repeat myData)
myResult = execState ((traverse . traverse) sumIfEven myData) 0
traverse
, like many many haskell concepts are extremely unintuitive on first glance, due to how abstract they are. But as you slowly build up a foundation and intuition on these generalized abstractions, they become very powerful tools in your arsenal.