When do you use Data.Monoid.Endo?
Data.Monoid module exports Endo which wraps a -> a. What’s this?
When it comes to a Monoid instance related to a function, you might remember this instance (I merged Semigroup into Monoid) for simplicity.
instance Monoid b => Monoid (a -> b) where
(f <> g) x = f x <> g x
mempty = const mempty
This instance calls both functions with the same argument and apply () to the results.
On the other hand, a Monoid instance of Endo composes functions.
instance Monoid (Endo a) where
Endo f <> Endo g = Endo $ f . g
mempty = Endo id
For example, x is [1, 3, 2, 3], but y is [1, 2, 3].
x :: [Int]
x = ((1:) (2:)) [3]
y :: [Int]
y = appEndo (Endo (1:) Endo (2:)) [3]
So when do we use it? One of the use cases Endo becomes handy is building a list. Imagine you have a function squash like this. This function takes a list of Monoid values and concatenates them, then pass it to a function to get a final result.
squash :: Monoid a => [a] -> (a -> b) -> b
squash xs f = f (mconcat xs)
You can pass [[1], [2], [3], ....] to concatenate it like this.
slow :: [Int]
slow = squash (map (:[]) [1..10000000]) id
But this will run in quadratic time. By using Endo, you can avoid this quadratic complexity.
fast :: [Int]
fast = squash (map (Endo . (:)) [1..10000000]) (flip appEndo [])
This will construct a composed function like (1:) . (2:) . (3:) . ..., and passing an empty list to this function will return the final result.