Skip to the content.

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.