Concepts: Profunctor

1. Motivation

The Functor interface provides an extremely useful method of transforming the output of a consumer. That handles one half of the problem of fitting consumers together into pipelines; making the output appropriate for the next stage. The other half of the problem is making the input appropriate for the next stage.

Suppose I have some selector to determine the props for a component. The component only needs the todos that are not done, in addition to any other props. The todos are embedded in an object though, and it would also be nice if I could keep them in an object structure as that is what is expected from a selector function. The function to get the done todos though does not know anything about the todos being in an object. Using the Profunctor interface I can define a method to "objectify" a consumer. I can pull a value off a key, apply a function to it, and return an object with the transformed value in that same key.

{profunctor-example-1 1}
{filter, 12:3}
const chores = (state, props) => state.chores;
// Returns { todos, groceries } object

const withoutDone = (todos) => filter((todo) => !todo.done, todos);

const selector =
  concat(chores, objectify('todos', withoutDone));

The final selector results in an object with a "todos" key that has the done todos filtered out of it.

2. Details

The Profunctor interface is really just an extension of the Functor interface.

{types/Profunctor.js 2}
export type Profunctor<Static, InA, InB, OutA, OutB> = {
  promap: ((InB => InA), (OutA => OutB), (InA, Static) => OutA) => (InB, Static) => OutB,
  mapInOut: ((InB => InA), (OutA => OutB), (InA, Static) => OutA) => (InB, Static) => OutB,
  {profunctor-types-mapIn, 2}
  ,
  {profunctor-types-mapOut, 2}
  ,
  {profunctor-types-objectify, 2}
};

The promap function takes two transforming functions, a consumer, and returns a new consumer. The first transformer takes some input and turns it into an input that the given consumer can accept. The second transformer works just like the transformer in the Functor map interface. As a more friendly name promap is aliased as mapInOut.

The laws promap needs to follow are similar to the Functor laws as well. The first is the identity law. It says that promapping two identity functions over a consumer is the same as just using that consumer.

{profunctor-laws-identity 2}
{identity, 12:3}
promap(identity, identity, consumer) === consumer

The second law is the composition law. It says that one promap with two function applications is the same as two promaps with one function application.

{profunctor-laws-composition 2}
promap(x => f(g(x)), y => h(i(y)), consumer) === promap(f, h, promap(g, i, consumer))

To satisfy these laws the implementation looks something like what follows. The input transformer is applied first, the consumer is applied next, and the output transformer is applied at the end.

{profunctor-promap-implementation 2}
const promap = (inF, outF, c) => (
  (s, a) => (
    outF(c(inF(s), a))
  )
);

In addition to the promap interface, there are some other convenience functions defined as part of the Profunctor interface. The first is mapIn.

{profunctor-types-mapIn 2}
mapIn: ((InB => InA), (InA, Static) => OutA) => (InB, Static) => OutA

The mapIn function only accepts a function to transform the input of the consumer. It is implemented using promap.

{profunctor-mapIn-implementation 2}
mapIn: (inF, c) => ProfunctorI.promap(inF, x => x, c)

The second convenience function is mapOut.

{profunctor-types-mapOut 2}
mapOut: ((OutA => OutB), (InA, Static) => OutA) => (InA, Static) => OutB

It is implemented as being the same as Functor's map function.

{profunctor-mapOut-implementation 2}
mapOut: Functor.map

The final convenience function is objectify, as discussed in the motivating example.

{profunctor-types-objectify 2}
objectify: (string, (InA, Static) => OutA) => ({[string]: InA}, Static) => {[string]: OutA }

It is also implemented in terms of promap.

{profunctor-objectify-implementation 2}
objectify: (k, c) => (
  ProfunctorI.promap((i) => i[k], (o) => ({ [k]: o}), c)
)

Finally all of this is wrapped up in a module.

{Profunctor.js 2}
{import-functor, 3:3}
{import-types-profunctor, 3}

{profunctor-promap-implementation, 2}
const ProfunctorI : Profunctor<*, *, *, *, *> = {
promap,
mapInOut: promap,
{profunctor-mapIn-implementation, 2}
,
{profunctor-mapOut-implementation, 2}
,
{profunctor-objectify-implementation, 2}
};

export default ProfunctorI;

3. Imports

The type definition for Profunctor needs to be imported.

{import-types-profunctor 3}
import type { Profunctor } from './types/Profunctor';

Used in section 2

Some modules require the Profunctor interface.

{import-profunctor 3}
import Profunctor from './Profunctor'

Used in sections 6:2 and 12:1


Previous ChapterNext Chapter