I’ve been working on some practical research projects lately, and so, I’ve been doing a ton of programming. In these projects I’ve been working with a lot of different data types and a particular programming pattern that I’ve been using over and over again has emerged.
The types of operations I’ve been working with have the shape
a -> D b
which can be viewed as an indexing into the
type D b
. Let’s consider an example, suppose we want to
define generalized union over sets. That is, given a function
i : a -> Set b
and a s : Set a
, then
define the union \(\bigcup_{x \in s}
(i\,x)\). We can define this function as follows:
indexedUnion :: (a -> Set b) -> Set a -> Set b
indexedUnion i = foldr (\x r -> (i x) `union` r) empty
We can see by this definition that we are essentially mapping the indexed operation over the set and folding, but we do this at the same time instead of going over the set twice.
Here is another example, suppose we want to take the conjunction of
the application an indexed operation i : a -> Bool
over the set s : Set a
. Then we can define this function
as follows:
indexedAnd :: (a -> Bool) -> Set a -> Bool
indexedAnd i = foldr (\x r -> (i x) && r) True
So at this point we can see a pattern emerging. Both of these functions can be defined by a combinator of type:
Foldable s => (b -> b -> b)
-> b
-> (a -> b)
-> s a
-> b
This is an instance of the well known foldMap
function
on monoids:
foldMap :: Monoid m => (a -> m) -> t a -> m
where we get our examples above if we take the monoid on sets to be
union and the empty set, and conjunction and True
for the
monoid on booleans.
However, consider a third example where we have an operation
i : a -> b
and a set s : Set a
, and we
want to map over the set s
, but we want to do this using
the accumulator pattern. Then we can define this function as
follows:
indexedInsert :: (a -> b) -> Set a -> Set b
indexedInsert i = foldl (\a x -> (i x) `insert` a) empty
This uses the same programming pattern, but has a slightly more general type:
indexedOp :: Foldable s
=> (c -> b -> b)
-> b
-> (a -> c)
-> s a
-> b
indexedOp op start i = foldl (\a x -> (i x) `op` a) start
Here we can see that this combinator differs from
foldMap
in that the binary operation we are using to fold
has different types for each argument, and we index into the first.
It’s easily defined using foldr
as well:
indexedOp' :: Foldable s
=> (c -> b -> b)
-> b
-> (a -> c)
-> s a
-> b
indexedOp' op start i = foldr (\x r -> (i x) `op` r) start
We can also define versions where we index into the second argument
as well. Notice that neither of these require op
to be
commutative, and in our third example, insert
is
non-commutative.
Every example above can be defined using this combinator, and I’ve used this operation to define a ton of programs. It’s a very useful combinator, and I thought it would make a nice short post.