Profunctors by Mark Seemann
Functors that are both co- and contravariant. An article for C# programmers.
This article series is part of a larger series of articles about functors, applicatives, and other mappable containers. Particularly, you've seen examples of both functors and contravariant functors.
What happens if you, so to speak, combine those two?
Mapping in both directions #
A profunctor is like a bifunctor, except that it's contravariant in one of its arguments (and covariant in the other). Usually, you'd list the contravariant argument first, and the covariant argument second. By that convention, a hypothetical Profunctor<A, B>
would be contravariant in A
and covariant in B
.
In order to support such mapping, you could give the class a DiMap
method:
public sealed class Profunctor<A, B> { public Profunctor<A1, B1> DiMap<A1, B1>(Func<A1, A> contraSelector, Func<B, B1> coSelector) // ...
Contrary to (covariant) functors, where C# will light up some extra compiler features if you name the mapping method Select
, there's no extra language support for profunctors. Thus, you can call the method whatever you like, but here I've chosen the name DiMap
just because that's what the Haskell Profunctors package calls the corresponding function.
Notice that in order to map the contravariant type argument A
to A1
, you must supply a selector that moves in the contrary direction: from A1
to A
. Mapping the covariant type argument B
to B1
, on the other hand, goes in the same direction: from B
to B1
.
An example might look like this:
Profunctor<TimeSpan, double> profunctor = CreateAProfunctor(); Profunctor<string, bool> projection = profunctor.DiMap<string, bool>(TimeSpan.Parse, d => d % 1 == 0);
This example starts with a profunctor where the contravariant type is TimeSpan
and the covariant type is double
. Using DiMap
you can map it to a Profunctor<string, bool>
. In order to map the profunctor
value's TimeSpan
to the projection
value's string
, the method call supplies TimeSpan.Parse
: a (partial) function that maps string
to TimeSpan
.
The second argument maps the profunctor
value's double
to the projection
value's bool
by checking if d
is an integer. The lambda expression d => d % 1 == 0
implements a function from double
to bool
. That is, the profunctor covaries with that function.
Covariant mapping #
Given DiMap
you can implement the standard Select
method for functors.
public Profunctor<A, B1> Select<B1>(Func<B, B1> selector) { return DiMap<A, B1>(x => x, selector); }
Equivalently to bifunctors, when you have a function that maps both dimensions, you can map one dimension by using the identity function for the dimension you don't need to map. Here I've used the lambda expression x => x
as the identity function.
You can use this Select
method with standard method-call syntax:
Profunctor<DateTime, string> profunctor = CreateAProfunctor(); Profunctor<DateTime, int> projection = profunctor.Select(s => s.Length);
or with query syntax:
Profunctor<DateTime, TimeSpan> profunctor = CreateAProfunctor(); Profunctor<DateTime, int> projection = from ts in profunctor select ts.Minutes;
All profunctors are also covariant functors.
Contravariant mapping #
Likewise, given DiMap
you can implement a ContraMap
method:
public Profunctor<A1, B> ContraMap<A1>(Func<A1, A> selector) { return DiMap<A1, B>(selector, x => x); }
Using ContraMap
is only possible with normal method-call syntax, since C# has no special understanding of contravariant functors:
Profunctor<long, DateTime> profunctor = CreateAProfunctor(); Profunctor<string, DateTime> projection = profunctor.ContraMap<string>(long.Parse);
All profunctors are also contravariant functors.
While you can implement Select
and ContraMap
from DiMap
, it's also possible to go the other way. If you have Select
and ContraMap
you can implement DiMap
.
Laws #
In the overall article series, I've focused on the laws that govern various universal abstractions. In this article, I'm going to treat this topic lightly, since it'd mostly be a reiteration of the laws that govern co- and contravariant functors.
The only law I'll highlight is the profunctor identity law, which intuitively is a generalisation of the identity laws for co- and contravariant functors. If you map a profunctor in both dimensions, but use the identity function in both directions, nothing should change:
Profunctor<Guid, string> profunctor = CreateAProfunctor(); Profunctor<Guid, string> projection = profunctor.DiMap((Guid g) => g, s => s);
Here I've used two lambda expressions to implement the identity function. While they're two different lambda expressions, they both 'implement' the general identity function. If you aren't convinced, we can demonstrate the idea like this instead:
T id<T>(T x) => x; Profunctor<Guid, string> profunctor = CreateAProfunctor(); Profunctor<Guid, string> projection = profunctor.DiMap<Guid, string>(id, id);
This alternative representation defines a local function called id
. Since it's generic, you can use it as both arguments to DiMap
.
The point of the identity law is that in both cases, projection
should be equal to profunctor
.
Usefulness #
Are profunctors useful in everyday programming? So far, I've found no particular use for them. This mirrors my experience with contravariant functors, which I also find little use for. Why should we care, then?
It turns out that, while we rarely work explicitly with profunctors, they're everywhere. Normal functions are profunctors.
In addition to normal functions (which are both covariant and contravariant) other profunctors exist. At the time that I'm writing this article, I've no particular plans to add articles about any other profunctors, but if I do, I'll add them to the above list. Examples include Kleisli arrows and profunctor optics.
The reason I find it worthwhile to learn about profunctors is that this way of looking at well-behaved functions shines an interesting light on the fabric of computation, so to speak. In a future article, I'll expand on that topic.
Conclusion #
A profunctor is a functor that is covariant in one dimension and contravariant in another dimension. While various exotic examples exist, the only example that you'd tend to encounter in mainstream programming is the Reader profunctor, also known simply as functions.
Next: Reader as a profunctor.
Comments
I have not found the concept of a profunctor useful in everyday programming, but I have found one very big use for them.
I am the maintainer of Elmish.WPF. This project makes it easy to create a WPF application in the style of the Model-View-Update / MVU / Elm Architecture. In a traditional WPF application, data goes between a view model and WPF via properties on that view model. Elmish.WPF makes that relationship a first-class concept via the type
Binding<'model, 'msg>
. This type is a profunctor; contravariant in'model
and covariant in'msg
. The individual mapping functions areBinding.mapModel
andBinding.mapMsg
respectively.This type was not always a profunctor. Recall that a profunctor is not really just a type but a type along with its mapping functions (or a single combined function). In versions 1, 2, and 3 of Elmish.WPF, this type is not a profunctor due to the missing mapping function(s). I added the mapping functions in version 4 (currently a prelease) that makes
Binding<'model, 'msg>
(along with those functions) a profunctor. This abstraction has made it possible to significantly improve both the internal implementation as well as the public API.As you stated, a single function
a -> b
is a profunctor; contravariant ina
and covariant inb
. I think of this as the canonical profunctor, or maybe "the original" profunctor. I think it is interesting to compare each profunctor to this one. In the case ofBinding<'model, 'msg>
, is implemented by two functions: one from the (view) model to WPF with type'model -> obj
that we could calltoWpf
and one from WPF with typeobj -> 'msg
that we could callfromWpf
. Of course, composing these two functions results in a single function that is a profunctor in exactly the way we expect.Now here is something that I don't understand. In addition to
Binding.mapModel
andBinding.mapMsg
, I have discovered another function with useful behavior in the functionBinding.mapMsgWithModel
. Recall that a functiona -> b
is not just profunctor in (contravariant)a
and (covariant)b
, but it is also a monad inb
(witha
fixed). The composed functiontoWpf >> fromWpf
is such a monad andBinding.mapMsgWithModel
is its "bind
" function. The leads one to think thatBinding<'model, 'msg>
could be a monad in'msg
(with'model
fixed). My intuition is that this is not the case, but maybe I am wrong.Tyson, it's great to learn that there are other examples of useful profunctors in the wild. It might be useful to add it to a 'catalogue' of profunctor examples, if someone ever compiles such a list - but perhaps it's a little to specific for a general-purpose catalogue...
After much digging around in the source code you linked, I managed to locate the definition of Binding<'model, 'msg>, which, however, turns out to be too complex for me to do a full analysis on a Saturday morning.
One of the cases, however, the TwoWayData<'model, 'msg> type looks much like a lens - another profunctor example.
Might
Binding<'model, 'msg>
also form a monad?One simple test I've found useful for answering such a question is to consider whether a lawful
join
function exists. In Haskell, thejoin
function has the typeMonad m => m (m a) -> m a
. The intuitive interpretation is that if you can 'flatten' a nested functor, then that functor is also a monad.So the question is: Can you reasonably write a function with the type
Binding<'model, Binding<'model, 'msg>> -> Binding<'model, 'msg>
?If you can write a lawful implementation of this
join
function, theBinding<'model, 'msg>
forms a monad.Thank you for linking to the deinition of
Binding<'model, 'msg>
. I should have done that. Yes, theTwoWayData<'model, 'msg>
case is probably the simplest. Good job finding it. I have thought about implementing such ajoin
function, but it doesn't seem possible to me.