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
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
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
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
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
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.