Haskell's strength: generalising with lenses
This post is about the strength
function, Lenses,
and strong functors.
Specifically, it’s about how we can generalise
strength
using lenses to work on any product type, not just tuples.
If you like, you can skip straight to the good bit where we derive a generalised strength.
For our purposes, strength
is a function which “everts” a tuple
containing some functor, (a, f b)
, turning it inside out to result
in a functor of a tuple f (a, b)
.
We can write this function as follows:
strength :: Functor f => (a, f b) -> f (a, b)
= fmap (\b -> (a, b)) fb strength (a, fb)
Note that the choice of f b
being the second item in the tuple is
fairly arbitrary, and we could just as easily have put it as the first.
Our choice is important to make code cleaner later in the article.
In Category Theory, a functor allowing this operation is said to be
strong.
A more formal definition of a strong functors can be found in Edward
Kmett’s excellent article
“Deriving strength from laziness”,
which also notes that all Haskell Functor
s are strong.
So what?
This might seem a bit pointless, but turns out to be useful in a number
of cases. For example, take MapReduce.
a MapReduce program defines two functions,
map
, and reduce
.
Haskell types for these functions might look like this:
mrMap :: ka -> va -> [(kb, vb)]
mrReduce :: kb -> [vb] -> [output]
where ka
denotes “key type a”, equivalent to in_key
from the
MapReduce slides, and “vb” denotes “value b”, equivalent to
intermediate_value
.
Let’s suppose we want to write the “identity” reduce function. That is,
a function which yields the output of the mrMap
function unchanged.
One such implementation is simply
mrReduce' :: kb -> [vb] -> [(kb, vb)]
= map (\v -> (k, v)) vs mrReduce' k vs
But it’s already looking very familiar. We can write that in terms of
strength
like this:
mrReduce :: kb -> [vb] -> [(kb, vb)]
= strength (k, vs)
mrReduce k vs
-- Or even more simply...
= curry strength mrReducePF
Another example: JSON objects to key/value pairs
In case you’re not particularly convinced, here’s another example.
Let’s say we’re dealing with a JSON object, and we want to parse it into a list of keys and values. An example object might look like this:
{
"foo": 3,
"bar": 1,
"baz": 4
}
And we want to end up with something like this:
result :: [(Text, Int)]
= [("foo", 3), ("bar", 1), ("baz", 4)] result
We’ll use the excellent Data.Aeson
library,
which provides a function
parseJSON :: FromJSON a => Value -> Parser a
which we can use to encode and decode to and from text.
We can use this, combined with strength
, to convert an Object- a
HashMap Text Value
- into a list of key/value pairs.
First, we’ll tackle converting a single key/value pair:
parsePair :: FromJSON v => (Text, Value) -> Parser (Text, v)
= strength . over _2 parseJSON parsePair
Now we can simply apply it to each of the object’s properties and combine the monadic results using mapM:
parseKVP :: FromJSON v => Value -> Parser [(Text, v)]
Object ps) = mapM parsePair $ toList ps
parseKVP (= mzero parseKVP _
What if it’s not a pair?
We’ve seen that our examples worked out particularly well for us, but only because we picked an implementation of strength matching our use-cases. How would we deal with nested Functors as the first item? For that matter, what about 3- and 4-tuples? In fact, what about records?
What we want is to be able to use strength
on any product type,
including records, with some easy way to specify which field is the
functor we want to evert.
Lenses to the rescue
Fortunately, the lens package is here to help. This gives us
a nice way of passing around getters and setters, and crucially lets
us do polymorphic updates. That means that when modifying a field, we
can also change its type
(and therefore also the type of the ‘parent’ record).
We’ve seen this already in the definition of parsePair
, where the
second item in a tuple was modified “in place”, while changing its type
from Value
to Parser v
.
The essence of strength
Clearly, our new function won’t be able to use pattern matching. We’ll rewrite the original function without pattern matching first.
strength' :: Functor f => (a, f b) -> f (a, b)
= fmap (g x) (f x)
strength' x where f x = snd x
= \b -> (fst x, b) g x
Note how it’s of the form fmap (g x) (f x)
, where:
f x
extracts the functor from the recordg x
is a function putting values of typeb
into a tuple
We can look at f and g as getters and setters of sorts, which is exactly what lenses do best. Now we can rewrite it like this:
strength'' :: Functor f => (a, f b) -> f (a, b)
= fmap (g x) (f x)
strength'' x where f = view _2
= \b -> set _2 b x g x
The good bit
From here it’s pretty clear how to proceed: we just need to replace the hard-coded lens with one passed in as an argument. Note that you’ll need the RankNTypes extension for this to work (i’m not sure why!)
strong :: Functor f => Lens s t (f a) a -> s -> f t
= fmap (set l ?? x) (view l x) strong l x
Note we’ve replaced the lambda expression using ??
, an infix version
of flip
which you can read as a placeholder for a missing argument.
Finally, to explain the type signature. The type Lens s t a b
can be
read as
“A lens into a record of type s
to a field of type ‘a’. The field can
be modified to be of type ‘b’, resulting in a record of type ‘t’.”
What we’ve specified is a Lens s t (f a) a
, which is a lens on a
record s
, containing a field of type f a
. The “target” record is of
type t
, and has a field of type a
- the type of elements of the
functor. Our strong
function now returns an f t
, the wrapped record.
Finally, some examples of usage:
Just "hi", 1) == Just ("hi", 1)
strong _1 ("key", [1..3]) == [("key", 1), ("key", 2), "key", 3)]
strong _2 (
data KVP k v = KVP
_key :: k
{ _val :: v
,
}'KVP
makeLenses '
KVP "key" [1..3]) == [KVP { _key = "Hi", _val = 1 } ... ] strong val (
Pretty cool!
Further down the rabbit hole
This isn’t the end of the story, however. If we examine the type of
strong
more closely, we find something interesting.
The Lens
type is defined thusly:
type Lens s t a b = Functor f => (a -> f b) -> s -> f t
If we replace these types with those from our earlier definition
of strong
, we find that
Lens s t (f a) a == Functor f => (f a -> f a) -> s -> f t
Which means that given a lens of this type, we can apply it to the identity function, and obtain the very same function:
strong' :: Lens s t (f a) a -> s -> f t
= l id strong' l
As it turns out, this function is already in Control.Lens, as the
sequenceAOf
function, which goes even further to work
on LensLike
lenses which don’t even require the Functor constraint
for f
.