Generalizing functions with ProfunctorsOscar PonceBlockedUnblockFollowFollowingFeb 8Nowadays functional programming has been gaining some deserved hype, and we read here and there about pure functions.
However, pure functions don’t seem that useful when we face real programming issues like write into a database; connect to a server given a URL and retrieve some data from it; or listen to actions from users in a GUI.
In all these scenarios we need side effects, we need impure functions.
As soon as we get into impure functions, all our mathematical tooling seems to disappear.
To help us in our quests we have some patterns as functors, monads, and profunctors.
Profunctors are similar to functions and can be a suitable replacement for the function abstraction where side effects are essential and we want to keep the idea of a process.
This is my third post related to profunctors.
After writing the other two I realized there were missing an introduction, and motivation on what profunctors are.
This new post tries to fill that gap.
Sometimes there is too much abstractionAllow me to call functions only those things that functional programmers call pure functions, that is, computations with no side effects.
These functions resemble quite what in mathematics is called merely function.
Functions are an abstraction, they can be used as a map: they represent a link between two places, like an IKEA id that tells us where in the warehouse is a model of a product.
In this case, we have a function from the models in the exhibition floor to the shell locations in the warehouse.
It is worth noticing that the models and the shell locations both exist independently of the function, they are just there.
The function doesn’t do anything to them; it’s just a map: this table is in that place, and this chair is in that other place.
Functions can also be used to model a production process — a machine.
For example, we put a coin into a candy machine, afterwards we get back a colorful candy ball, in which case we can say that the candy machine is a function from coins to candies.
If we are interested in what we can make (or where we can get to) starting with (or from) something else, functions are perfect abstractions.
They take away all those distracting details as how long do I have to walk on the warehouse to get the chair, or how heavy it will be; or that we need to provide mechanical energy while cranking at the candy machine, and that it may fail to deliver.
Many functions in mathematics are, in a sense, only theoretical.
For example, a function may give for granted that if there is an infinite process we know how to perform, we can consider we have the result right now.
If such a function was the model for a machine, there is no way we can ever get a result from it as it takes infinite time to produce it.
In cases when there is no pragmatic realization of the process modeled by a function the abstraction may backfire us if we are concerned about a real machine or a process a human can perform in reasonable time.
Removing abstraction but not allSometimes, context is essential, and we need to trade off some abstraction power in exchange for a more accurate representation of the system we are modeling.
Let’s go back to the candy machine example.
There are a bunch of details we may want to represent: the candies may get stuck, and nothing will come out after cranking the handle; or the machine is probably empty, in which case no candy will come out either; or the coin is too big or too small; or we may want to know how many turns we need to perform on the handle because probably we are adding a motor to it to make it more fashionable.
As there are details we now want to know about a machine or a process, there are some others that we still want to keep hidden so they don’t distract us, like: how much the machine costs, where is it, what color is it, what colors are the candies and so on.
Mathematics has many abstractions besides functions, one of them is called profunctor, and that’s the one I want to present in this text as an alternative to functions for those cases where we care about the context of the process.
Profunctors appear in Category Theory, a branch of mathematics that some people have called the mathematics of mathematics (Cheng2015) (they also have been called “abstract nonsense” by some of the most famous mathematicians in the field Mac-Lane1997 ).
Impure functions and profunctorsImagine we have a machine that can assemble egg cartons with 12 eggs each and that can take one egg at a time.
The machine doesn’t produce any carton until it has enough eggs to fill it.
We put the first egg, and nothing happens, the second, the third, and eventually we put in a twelfth egg, and the machine starts to clank and shake and gently throws an egg carton into the output tray.
In programming, we say this function has state and that it’s impure as sometimes it returns nothing and sometimes returns an egg carton.
In functional programming jargon, when a function keeps state as the egg carton machine in the last example, we call it impure, a function with side effects or stateful computation.
Examples of these functions are functions that return random numbers, values that depend on external conditions, values that take time to compute (Futures or Promises) or that return more than one value (Streams); as well as functions that write on the disc or perform network calls while producing a value.
Many of these functions can be described as profunctors.
Mathematical functions can not model computations where the context is essential.
Side effects are the most important feature that stateful computations have.
Without their side effects a program is useless — we care about programs because they can transform the world they inhabit (the computer).
Profunctors, on the other side, can model perfectly well those additional contexts.
Impurity is compatible with the profunctor abstraction.
Why do we care about profunctors?Because impure functions are not functions in the mathematical sense, we cannot use all the theory that is already available for functions, for example using equational reasoning to improve the performance of a program is only safe if we deal with pure functions.
Impure functions place us where we don’t have all those techniques from mathematics unless we can find our way back to abstractions.
Profunctors are precisely that, they are our ticket back to a mathematically ruled world were we can model and operate using a powerful infrastructure built by mathematicians, while keeping context information and side effects.
Although with profunctors we loose some tools like equational reasoning, we still have compositionality, and if we add some more restrictions, we can have many ways to transform them and to play with them.
Let’s go back to our candy machine example where we put a coin and get a candy:We now want to represent the case where sometimes there is no candy (the machine got stuck, or there were no more candies):What if we could transform our candy machine with a retry transformation that when there is no candy, it tries again until it gets a candy?We may want to add an extra functionality that wraps the candy in a lovely box, but because we are doing so much now we may want to charge a bit more than a single coin, so we need to add a coin exchange transformation to the input:More concrete examples of profunctors are APIs returning a future or a stream with the data, IO actions, functions returning optional or random values.
I wrote two posts about how to use profunctor optics to transform and extend profunctors (Ponce2018–1, Ponce2018–2), there are also great posts on the web (López-González2018, Milewski2017) you may find interesting and that I list at the end.
Dissecting a profunctorThis section is a bit more technical, and you can safely skip it.
If you are interested in the categorical definition of profunctors, there is an excellent nLab article you can read as well as some other references that I list at the end.
What would be a useful abstraction to represent such processes together with their context?.Another almost similar question is how much of the function abstraction we want to keep?.These are two characteristics profunctors keep from the old abstraction:Profunctors as functions have a source type and a target type.
That is, for describing a function we need to start by defining what we put in as input and what we get out.
We say the function f :: a → b takes a values and produces b values.
Profunctors as functions can be transformed: a function f :: a → b can be transformed into a function g :: c → d if we provide two additional functions too:: c → a and from :: b → d from where we can just derive g as:g :: c → d = too ▹ f ▹ fromWe’ll call this double transformation dimap:g :: c -> d = dimap too from fAnd dimap is well behaved when used with the identity function and composition:f ≅ dimap id id f ≅ id ▹ f ▹ idIf here are two other functions too’ :: e → c and from’ :: d → fdimap (too’ ▹ too) (from ▹ from’) f ≅ dimap too’ from’ (dimap too from f)A definition of profunctor looks like:class Profunctor p where dimap :: (a → b) → (c → d) → p b c → p a dBefore we continue, we need to verify that our abstraction makes sense, which means that regular pure functions are profunctors.
After all, we came into profunctors because we wanted an abstraction where functions where a particular case but that included a lot more of different things.
What would be an implementation for dimap in regular functions?.We already sketched it when first came to the idea of dimap (I’m using PureScript notation for composition ▹ as to me it is easier to read):instance Profunctor (→) where dimap to from f = to ▹ f ▹ fromAre there any other examples of profunctors besides regular functions?.Yes, plenty of them, actually, for every functor f a function h that can produce functorial values on f is a profunctor:h :: a → f bh’ = dimap to from h = to ▹ h ▹ fmap from –:: c → f dWhich also means that any Kliesli arrow is also a profunctor (a Kliesli arrow is a function a → m b where m is a monad).
A similar construction can be made with contravariant functors.
If f is a contravariant functor:h :: f a → bh’ = dimap to from h = contramap to ▹ h ▹ from –:: f c → dAnd of course, if we have a functor f and a contravariant g:h :: g a → f bh’ = dimap to from h = contramap to ▹ h ▹ fmap from –:: f c → g dConclusionProfunctors are an alternative abstraction to functions where we are interested in the context of a computation; and because they still obey some rules, we can use much of the theoretical infrastructure already made in mathematics.
Profunctors can be composed and transformed in ways that can be easy to reason about or to test like when using profunctor optics.
References[Cheng2015] Eugenia Cheng, 2015, How to Bake Pi: An Edible Exploration of the Mathematics of Mathematics.
[Mac-Lane1997] Saunders Mac Lane, 1997, The PNAS way back then.
[nLab2018] nLab, 2018, Profunctor.
[López-González2018] Jesús López-González @jeslg, 2018, Don’t Fear the Profunctor Optics![Milewski2017] Bartosz Milewski, 2017, Profunctor Optics: The Categorical View.
[Ponce2018–1] Oscar Ponce, 2018, Another Look Through Optics.
[Ponce2018–2] Oscar Ponce, 2018, Handling Errors with Profunctor Optics.