Documente Academic
Documente Profesional
Documente Cultură
Functor Functors
Posted on December 15, 2017
One of the fun things about category theory is that once you’ve learned an idea in one context it’s easy to
apply it to another one. Of the numerous categories available to Haskell programmers, Hask, the category of
Haskell types and functions, gets the lion’s share of the attention. Working with standard abstractions in
more overlooked categories is a great way to reuse ideas: it makes you look clever, like you’ve invented
something new, but actually all you’ve done is put the building blocks together differently. I won’t tell if you
don’t.
Now, the standard trick to de-duplicate these two types is to derive both from what I’ll call a template type,
wrapping each field of the template in some type constructor f. You recover Form by setting f to the boring
Identity functor, and you get DraftForm by setting f to Maybe.
So a template is a record type parameterised by a type constructor. It’ll generally have a kind of (* -> *)
-> *. The fields of the record are the type constructor applied to a variety of different type arguments.
Working with a template typically involves coming up with an interesting type constructor (* -> *) and
plugging it in to get interestingly-typed fields. You can think of a record as a container of fs.
This trick has become Haskell folklore - I couldn’t tell you where I first saw it - but I’ve only seen a few
people talk about what happens when you treat templates as first class citizens. To get used to this style, a
simple example is giving names to specific instantiations of arbitrary templates:
The rest of this blog post is about treating template types intuitively as fixed-size containers of functors. I’ll
be taking familiar tools for working with containers of values - Functor, Traversable,
Representable - and applying them to the context of containers of functors.
We’re talking about record templates of kind (* -> *) -> *. This fits the pattern of a functor from the
functor category, with k1 ~ k2 ~ k3 ~ *. So the functor category in question is the category of
endofunctors on Hask (that is, members of the standard Functor class), and the destination category is
Hask. So it’s reasonable to expect record templates to be functorial in their argument:
FFunctor comes with the usual functor laws. The only difference is the types.
-- identity
ffmap id = id
-- composition
ffmap (eta . phi) = ffmap eta . ffmap phi
ffmap encodes the notion of generalising the functor a template has been instantiated with. If you can
embed the functor f into g, then you can map a record of fs to a record of gs by embedding each f. (This is
also sometimes called “hoisting”.) For example, the boring Identity functor can be embedded into an
arbitrary Applicative by injecting the contained value using pure. We can use this to turn a total record
into a partial one:
Traversing Records
Now that we have a new dog, it’s natural to ask which old tricks we can teach it. With the intuition that a
template t f is like a container of fs, what does it mean to traverse such a container? sequenceA ::
Applicative f => t (f a) -> f (t a) takes a container of strategies to produce values and
sequences them to get a strategy to produce a container of values. Replacing value with functor in the above
sentence, it’s clear that we need to decide on a notion of “strategy to produce a functor”. With thanks to Li-
yao Xia, the simplest of such notions is a regular applicative functor a returning a functorial value g x - that
is, Compose a g.
The FTraversable laws come about by adjusting the Traversable laws to add some Compose-
bookkeeping.
-- naturality
nu . ftraverse eta = ftraverse (Compose . nu . getCompose . eta)
-- for any applicative transformation nu
-- identity
ftraverse (Compose . Identity) = Identity
-- composition
ftraverse (Compose . Compose . fmap (getCompose.phi) . getCompose . eta)
= Compose . fmap (ftraverse phi) . ftraverse eta
Implementations of traverse look like implementations of fmap but in an applicative context. Likewise,
implementations of ftraverse look like implementations of ffmap in an applicative context, with a few
getComposes scattered around.
This is where things start to get interesting. The toForm function, which converts a draft form to a regular
form if all of its fields have been filled in, can be defined tersely in terms of ftraverse.
Here’s another example: a generic program, defined by analogy to Foldable’s foldMap, to collapse the
fields of a record into a monoidal value. Note that f () -> m is isomorphic to, but simpler than, forall
x. f x -> m. Annoyingly, we have to give a type signature to mkConst to resolve the ambiguity over g
in the call to ftraverse. I’m picking Empty as a way of demonstrating that I have nothing up my
sleeves.
Of course, we’re working with functors from the functor category, so the relevant notion of
Representable will need a little adjustment. Instead of an isomorphism to a function (->) r we’ll use
an isomorphism to a natural transformation (~>) r.
fzipWith :: FRepresentable t
=> (forall x. f x -> g x -> h x)
-> t f -> t g -> t h
fzipWith f t u = ftabulate $ \r -> f (findex t r) (findex u r)
fzipWith3 :: FRepresentable t
=> (forall x. f x -> g x -> h x -> k x)
-> t f -> t g -> t h -> t k
fzipWith3 f t u v = ftabulate $
\r -> f (findex t r) (findex u r) (findex v r)
The laws for FRepresentable simply state that ftabulate and findex must witness an
isomorphism:
-- isomorphism
ftabulate . findex = findex . ftabulate = id
FRep will typically be a GADT: it tells you what type of value one should expect to find at a given position
in a record.
Something useful you can do with this infrastructure: filling in defaults for missing values of a partial record.
Or, looking at it the other way, overriding certain parts of a record.
You can also make a record of Monoid values into a Monoid, once again by zipping.
Lenses
Rather than come up with a new notion of Lens formulated in terms of FFunctor, we can reuse the
standard Lens type as long as we’re careful about how polymorphic lenses should be. Specifically, a lens
into a record template should express no opinion as to which functor the template should be instantiated
with.
makeLenses ''FormTemplate
instance HasLenses FormTemplate where
lenses = FormTemplate {
_email = FLens email,
_cardType = FLens cardType,
_cardNumber = FLens cardNumber,
_cardExpiry = FLens cardExpiry
}
Compositional Validation
Now for an extended example: form validation. We’ll be making use of all of the tools from above - zipping,
traversing, and mapping - to design a typed API for validating individual fields of a form.
Either isn’t a great choice for a validation monad, because Either aborts the computation at the first
failure. You typically want to report all the errors in a form. Instead, we’ll be working with the following
type, which is isomorphic to Either but with an Applicative instance which returns all of the failures
in a given computation, combining the values using a Monoid. So it’s kind of a Frankensteinian mishmash
of the Either and Writer applicatives.
To get started, we’ll build a library for validation processes which examine a single field of a record at a
time. Later we can extend it to support context-sensitive validation rules like “the format of the card number
must match the card type”. A validation rule for a field typed a is a function which takes an a and returns a
Validation e a.
If a given field has multiple validation rules, you can compose them under the assumption that each validator
leaves its input unchanged.
-- for example
emailValidator :: Validator [Text] Text
emailValidator = hasAtSymbol &> hasTopLevelDomain
where
hasAtSymbol = Validator $ \email ->
if "@" `isInfixOf` email
then Success email
else Failure ["No @ in email"]
hasTopLevelDomain = Validator $ \email ->
if any (`isSuffixOf` email) topLevelDomains
then Success email
else Failure ["Invalid TLD"]
topLevelDomains = [".com", ".org", ".co.uk"] -- etc
The plan is to store these Validators in a record template, zip them along an instance of the record itself,
and then traverse the result to get either a validated record or a collection of errors. To make things
interesting, we’ll store the validation results for a given field in the matching field of another record.
applyValidator takes a lens into a record field, a validator for that field and the value in that field. It
applies the validator to the value; upon failure it stores the error message (e) in the correct field of the
Errors record using the lens. fzipWith3 handles the logic of running applyValidator for each
field of the record, then fsequence' combines the resulting Validation applicative actions into a
single one. So all of the errors from all of the fields are eventually collected into the matching fields of the
Errors record and combined monoidally.
Code review
So we have a categorical framework for working with records and templates. Other things fit into this
framework, more or less neatly:
One design decision I made when developing the FFunctor class was to give ffmap a (Functor f,
Functor g) constraint, so you can only ffmap between types that are in fact functors. This is
mathematically principled in some sense, but it has certain engineering tradeoffs compared to an
unconstrained type for ffmap. It enables more instances of FFunctor - for example, you can only write
Fix’s ffmap with a Functor constraint for either the input or output type parameters - but it rules out
certain usages of ffmap. You can’t ffmap over a template containing Validators, for example, because
Validator is not a Functor. I didn’t put the same Functor constraints into FRepresentable’s
methods. An FRep type typically won’t be functorial - it’ll be GADT-like - so adding a Functor (FRep
t) constraint would be far too restrictive.
You’ll notice that the concept of an applicative functor functor is conspicuously absent from my presentation
above. FApplicative would probably look something like this:
How useful are these tools in practice? Would I structure a production application around functor functors?
Probably not. It’s a question of balance - while it’s useful to recognise functorial structures in categories
other than Hask as a thinking tool, actually representing such abstractions in code doesn’t always pay off.
Haskell already has a rich ecosystem of tools for working with the Functor family, but there’s much less
code in the wild that’s structured around functor functors. This is partly because Functor has the
advantage of being a standard class in base, but it’s also because code built around functor functors is a
little less convenient to work with, typically requiring some tedious newtype bookkeeping.
Over the course of putting together this article I came across some work by others on this very topic. I’ve
spotted versions of these classes being packaged with bigger libraries such as hedgehog and
quickcheck-state-machine. There are also a few packages providing similar tools. The most mature
of these seems to be rank2classes, which includes some Template Haskell tools for deriving instances;
there’s also the Conkin package, which has a well-written tutorial focusing on working with data in column-
major order.
Haskell’s full of big ideas and powerful programming idioms. In this post we saw an example of
reinterpreting some familiar tools - Functor, Traversable and Representable - in a new context.
With the intuition that a record template is a container of functors, and the formalism of functors from the
functor category, we were able to reuse intuitions about those familiar tools to write terse and generic
programs.
Comments