4

The algebraic structure of functions, illustrated using React components

 4 years ago
source link: https://jrsinclair.com/articles/2020/algebraic-structure-of-functions-illustrated-with-react-components/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Did you know there’s an algebraic structure for functions? That may not surprise you at all. But it surprised me when I first found out about it. I knew we used functions to build algebraic structures. It never occurred to me that functions themselves might have an algebraic structure.

I should clarify though. When I use the word ‘function’ here, I mean function in the functional programming sense. Not in the JavaScript sense. That is, pure functions; no side effects; single input; always return a value; and so on… You know the drill. Also, I’m going to assume you understand referential transparency and composition. If not, check out A gentle introduction to functional JavaScript . It might also help if you’ve read How to deal with dirty side effects in your pure functional JavaScript .

How does this algebraic structure for functions work? Well, recall our idea of eventual numbers when we looked atEffect. They looked something like this:

const compose2  = f => g => x => f(g(x));
const increment = x => x + 1;
const double    = x => x * 2;

const zero  = () => 0;
const one   = compose2(increment)(zero);
const two   = compose2(double)(one);
const three = compose2(increment)(two);
const four  = compose2(double)(two);
// ... and so on.

In this way we could create any integer as an eventual integer. And we can always get back to the ‘concrete’ value by calling the function. If we call three() at some point, then we get back 3. But all that composition is a bit fancy and unnecessary. We could write our eventual values like so:

const zero  = () => 0;
const one   = () => 1;
const two   = () => 2;
const three = () => 3;
const four  = () => 4;

// … and so on.

Looking at it this way may be a little tedious, but it’s not complicated. To make a delayed integer, we take the value we want and stick it in a function. The function takes no arguments, and does nothing but return our value. And we don’t have to stop at integers. We can make any value into an eventual value. All we do is create a function that returns that value. For example:

const ponder  = () => 'Curiouser and curiouser';
const pi      = () => Math.PI;
const request = () => ({
    protocol: 'http',
    host: 'example.com',
    path: '/v1/myapi',
    method: 'GET'
});

// You get the idea…

Now, if we squint a little, that looks kind of like we’re putting a value inside a container. We’ve got a bit of containery stuff on the left, and value stuff on the right. The containery stuff is uninteresting. It’s the same every time. It’s only the return value that changes.

Enter the functor

Could we make a Functor out of this containery eventual-value thing? To do that, we need to define a law-abiding map() function. If we can, then we’ve got a valid functor on our hands.

To start, let’s look at the type signature for map() . In Hindley-Milner notation, it looks something like this:

map :: Functor m => (a -> b) -> m a -> m b

This says that our map function takes a function, and a functor of a , and returns a functor of b . If functions are functors, then they would go into that m slot:

map :: (a -> b) -> Function a -> Function b

This says that map() takes a function from a to b and a Function of a . And it returns a Function of b . But what’s a ‘Function of a ’ or a ‘Function of b ’?

What if we started out with eventual values? They’re functions that don’t take any input. But they return a value. And that value (as we discussed), could be anything. So, if we put them in our type signature might look like so:

map :: (a -> b) -> (() -> a) -> (() -> b)

The a and b in the type signature are return value of the function. It’s like map() doesn’t care about the input values. So let’s replace the ‘nothing’ input value with another type variable, say t . This makes the signature general enough to work for any function.

map :: (a -> b) -> (t -> a) -> (t -> b)

If we prefer to work with a , b and c , it looks like this:

map :: (b -> c) -> (a -> b) -> (a -> c)

And that type signature looks a lot like the signature for compose2 :

compose2 :: (b -> c) -> (a -> b) -> a -> c

And in fact, they are the same function. The map() definition for functions is composition.

Let’s stick our map() function in aStatic-Land module and see what it looks like:

const Func = {
    map: f => g => x => f(g(x)),
};

And what can we do with this? Well, no more and no less than we can do with compose2() . And I assume you already know many wonderful things you can do with composition. But function composition is pretty abstract. Let’s look at some more concrete things we can do with this.

React functional components are functions

Have you ever considered that React functional components are genuine, bona fide functions? (Yes, yes. Ignoring side effects and hooks for the moment). Let’s draw a couple of pictures and think about that. Functions in general, take something of type \(A\) and transform it into something of type \(B\) .

jmYFfuv.png!web A function takes an input of some type A and returns an output value of some type B.

I’m going to be a bit sloppy with types here but bear with me. React functional components are functions, but with a specific type. They take Props and return a Node. That is, they take a JavaScript object return something that React can render.So that might look something like this:

JJn6JvU.png!web

Now consider map() / compose2() . It takes two functions and combines them. So, we might have a function from type \(B\) to \(C\) and another from \(A\) to \(B\) . We compose them together, and we get a function from \(A\) to \(C\) . We can think of the first function as a modifier function that acts on the output of the second function.

aUFrm2f.png!web

Let’s stick a React functional component in there. We’re going to compose it with a modifier function. The picture then looks like this:

AviQnuN.png!web

Our modifier function has to take a Node as its input. Otherwise, the types don’t line up. That’s fixed. But what happens if we make the return value Node as well? That is, what if our second function has the type \(Node \rightarrow Node\) ?

eYFBvi3.png!web

We end up with a function that has the same type as a React Function Component . In other words, we get another component back. Now, imagine if we made a bunch of small, uncomplicated functions. And each of these little utility functions has the type \(Node \rightarrow Node\) . With map() we can combine them with components, and get new, valid components.

Let’s make this real. Imagine we have a design system provided by some other team. We don’t get to reach into its internals and muck around. We’re stuck with the provided components as is. But with map() we claw back a little more power. We can tweak the output of any component. For example, we can wrap the returned Node with some other element:

import React from 'react';
import AtlaskitButton from '@atlaskit/button';

// Because Atlaskit button isn't a function component,
// we convert it to one.
const Button = props => (<AtlaskitButton {...props} />);

const wrapWithDiv   = node => (<div>{node}</div>);
const WrappedButton = Func.map(wrapWithDiv)(Button);

See it in a sandbox

Or we could even generalise this a little…

import React from "react";
import AtlaskitButton from "@atlaskit/button";

// Because Atlaskit button isn't a function component,
// we convert it to one.
const Button = props => <AtlaskitButton {...props} />;

const wrapWith = (Wrapper, props = {}) => node => (
    <Wrapper {...props}>{node}</Wrapper>
);
const WrappedButton = Func.map(
  wrapWith("div", { style: { border: "solid pink 2px" } })
)(Button);

See it in a sandbox

What else could we do? We could append another element:

import React from "react";
import AtlaskitButton from "@atlaskit/button";
import PremiumIcon from "@atlaskit/icon/glyph/premium";

// Because Atlaskit button isn't a function component,
// we convert it to one.
const Button = props => <AtlaskitButton {...props} />;

const appendIcon = node => (<>{node}<PremiumIcon /></>);
const PremiumButton = Func.map(appendIcon)(Button);

See it in a sandbox

Or we could prepend an element:

import React from 'react';
import Badge from '@atlaskit/badge';


const prependTotal = node => (<><span>Total: </span>{node}</>)
const TotalBadge = Func.map(prependTotal)(Badge);

See it in a sandbox

And we could do both together:

import React from 'react';
import StarIcon from '@atlaskit/icon/glyph/star';
import Button from '@atlaskit/button';

// Because Atlaskit button isn't a function component,
// we convert it to one.
const Button = props => <AtlaskitButton {...props} />;

const makeShiny = node => (
    <>
        <StarIcon label="" />{node}<StarIcon label="" />
    </>
);
const ShinyButton = Func.map(makeShiny)(Button);

See it in a sandbox

And all three at once:

import React from 'react';
import AtlaskitButton from "@atlaskit/button";
import Lozenge from '@atlaskit/lozenge';
import PremiumIcon from '@atlaskit/icon/glyph/premium';
import Tooltip from '@atlaskit/tooltip';

// Because Atlaskit button isn't a function component,
// we convert it to one.
const Button = props => <AtlaskitButton {...props} />;

const shinyNewThingify = node => (
    <Tooltip content="New and improved!"><>
        <PremiumIcon label="" />
        {node}
        <Lozenge appearance="new">New</Lozenge>
    </></Tooltip>
);

const ShinyNewButton = Func.map(shinyNewThingify)(Button);

const App = () => (
    <ShinyNewButton>Runcible Spoon</ShinyNewButton>
);

See it in a sandbox

Element enhancers

I call these \(Node \rightarrow Node\) functions Element enhancers .It’s like we’re creating a template. We have a JSX structure with a node-shaped hole in it. We can make that JSX structure as deep as we like. Then, we use Func.map() to compose the element enhancer with a Component. We get back a new component that eventually shoves something deep down into that slot. But this new component takes the same props as the original.

This is nothing we couldn’t already do. But what’s nice about element enhancers is their simplicity and re-usability. An element enhancer is a simple function. It doesn’t mess around with props or anything fancy. So it’s easy to understand and reason about. But when we map() them, we get full-blown components. And we can chain together as many enhancers as we like with map() .

I have a lot more to say about this, but I will save it for another post. Let’s move on and look at Contravariant Functors.

Contravariant functor

Functors come in lots of flavours. The one we’re most familiar with is the covariant functor. That’s the one we’re talking about when we say ‘functor’ without any qualification. But there are other kinds. The contravariant functor defines a contramap() function. It looks like someone took all the types for map() and reversed them:

-- Functor general definition
map :: (a -> b) -> Functor a -> Functor b

-- Contravariant Functor general definition
contramap :: (a -> b) -> Contravariant b -> Contravariant a

-- Functor for functions
map :: (b -> c) -> (a -> b) -> (a -> c)

-- Contravariant Functor for functions
contramap :: (a -> b) -> (b -> c) -> (a -> c)

Don’t worry if none of that makes sense yet. Here’s how I think about it. With functions, map() let us change the output of a function with a modifier function. But contramap() lets us change the input of a function with a modifier function. Drawn as a diagram, it might look like so:

Bb2UZfF.png!web

If we’re doing this with React components then it becomes even clearer. A regular component has type \(Props \rightarrow Node\) . If we stick a \(Props \rightarrow Props\) function in front of it, then we get a \(Props \rightarrow Node\) function back out. In other words, a new component.

Ur2Qzqv.png!web

So, contramap() is map() with the parameters switched around:

const Func = {
    map:       f => g => x => f(g(x)),
    contramap: g => f => x => f(g(x)),
};

Contramapping react functional components

What can we do with this? Well, we can create functions that modify props. And we can do a lot with those. We can, for example, set default props:

// Take a button and make its appearance default to 'primary'
import Button from '@atlaskit/button';

function defaultToPrimary(props) {
    return { appearance: 'primary', ...props};
}

const PrimaryButton = Func.contramap(defaultToPrimary)(Button);

See it in a sandbox

And, of course, we could make a generic version of this:

import Button from '@atlaskit/button';

function withDefaultProps(defaults) {
    return props => ({...defaults, ...props});
}

const PrimaryButton = Func.contramap(
    withDefaultProps({ appearance: 'primary' })
)(Button);

See it in a sandbox

If we want to, we could also hard-code some props so that nobody can change them. To do that we reverse our spread operation.

import Button from '@atlaskit/button';

function withHardcodedProps(fixedProps) {
    return props => ({...props, ...fixedProps});
}

const PrimaryButton = Func.contramap(
    withHardcodedProps({ appearance: 'primary' })
)(Button);

See it in a sandbox

You might be thinking, is that all? And it might not seem like much. But modifying props gives us a lot of control. For example, remember that we pass children as props. So, we can do things like wrap the inner part of a component with something. Say we have some CSS:

.spacer {
    padding: 0.375rem;
}

And imagine we’re finding the spacing around some content too tight. With our handy tool contramap() , we can add a bit of space:

import React from 'react';
import AtlaskitSectionMessage from '@atlaskit/section-message';

// Atlaskit's section message isn't a functional component so
// we'll convert it to one.
const SectionMessage = props => <AtlaskitSectionMessage {...props} />;

const addInnerSpace = ({children, ...props}) => ({
    ...props,
    children: <div class="spacer">{children}</div>
});

const PaddedSectionMessage = Func.contramap(addInnerSpace)(SectionMessage);

const App = () => (
    <PaddedSectionMessage title="The Lion and the Unicorn">
        <p>
        The Lion and the Unicorn were fighting for the crown:<br />
        The Lion beat the Unicorn all round the town.<br />
        Some gave them white bread, some gave them brown:<br />
        Some gave them plum-cake and drummed them out of town.
        </p>
    </PaddedSectionMessage>
);

See it in a sandbox

Functions as profunctors

Our contramap() function lets us change the input and map() lets us change the output. Why not do both together? This pattern is common enough that it has a name: promap() . And we call structures that you can promap() over, profunctors . Here’s a sample implementation for promap() :

const Func = {
    map:       f => g => x => f(g(x)),
    contramap: g => f => x => f(g(x)),
    promap:    f => g => h => Func.contramap(f)(Func.map(g)(h)),
};

Here’s an example of how we might use it:

import React from "react";
import AtlaskitTextfield from "@atlaskit/textfield";

// Atlaskit's Textfield isn't a function component, so we
// convert it.
const Textfield = props => <AtlaskitTextfield {...props} />;

const prependLabel = (labelTxt, id) => node => (
  <>
    <label htmlFor={id}>{labelTxt}</label>
    {node}
  </>
);

function withHardcodedProps(fixedProps) {
  return props => ({ ...props, ...fixedProps });
}

const id = "thamaturgical-identifier";
const lblTxt = "Please provide your thaumaturgical opinion:";

const ThaumaturgyField = Func.promap(withHardcodedProps({ id }))(
  prependLabel(lblTxt, id)
)(Textfield);

export default function App() {
  return (
    <div className="spacer">
      <ThaumaturgyField />
    </div>
  );
}

See it in a sandbox

With promap() we could tweak the props and the output of a React component in one pass. And this is pretty cool. But what if we wanted to change the output based on something in the input? The sad truth is that promap() can’t help us here.

Functions as applicative functors

All is not lost. We have hope. But first, why would we want to do this? Let’s imagine we have a form input. And rather than disable the input when it’s not available, we’d like to hide it entirely. That is, when the input prop disabled is true , then we don’t render the input at all. To do this, we’d function that has access to both the input and the output of a component. So, what if we passed the input (props) and output (node) as parameters? It might look like so:

// hideWhenDisabled :: Props -> Node -> Node
const hideWhenDisabled = props => node => (
    (props.isDisabled) ? null : node
);

Not all that complicated. But how do we combine that with a component? We need a function that will do two things:

hideWhenDisabled()

It might look something like this:

// mysteryCombinatorFunction :: (a -> b -> c) -> (a -> b) -> a -> c
const mysteryCombinatorFunction = f => g => x => f(x)(g(x));

And this mystery combinator function has a name. It’s called ap() . Let’s add ap() to our Func module:

const Func = {
    map:       f => g => x => f(g(x)),
    contramap: g => f => x => f(g(x)),
    promap:    f => g => h => Func.contramap(f)(Func.map(g)(h)),
    ap:        f => g => x => f(x)(g(x)),
};

Here’s how it might look as a diagram:

eyURfee.png!web

If we are working with react components, then it might look like so:

zUBjyy2.png!web

With that in place, we can use our hideWhenDisabled() function like so:

import React from "react";
import AtlaskitTextfield from "@atlaskit/textfield";

// Atlaskit's Textfield isn't a function component, so we
// convert it.
const Textfield = props => <AtlaskitTextfield {...props} />;

// hideWhenDisabled :: Props -> Node -> Node
const hideWhenDisabled = props => el => (props.isDisabled ? null : el);

const DisappearingField = Func.ap(hideWhenDisabled)(Textfield);

See it in a sandbox

Now, for a function to be a full applicative functor, there’s another function we need to implement. That’s of() . It takes any value and turns it into a function. And we’ve already seen how to do that. It’s as simple as making an eventual value:

// Type signature for of():
// of :: Applicative f => a -> f a

// For functions this becomes:
// of :: a -> Function a

// Which is the same as:
// of :: a -> b -> a

// We don’t care what the type of b is, so we ignore it.
const of = x => () => x;

Let’s stick that in our module:

const Func = {
    map:       f => g => x => f(g(x)),
    contramap: g => f => x => f(g(x)),
    promap:    f => g => h => Func.contramap(f)(Func.map(g)(h)),
    ap:        f => g => x => f(x)(g(x)),
    of:        x => () => x,
};

There’s not much advantage in using Func.of() over creating an inline function by hand. But it allows us to meet the specification. That, in turn, means we can take advantage of derivations and pre-written code. For example, we can use ap() and of() to derive map() :

const map = f => g => Func.ap(Func.of(f))(g);

Not all that useful, but good to know.

Functions as monads

One final thought before we wrap up. Consider what happens if we swap the parameter order for our hideWhenDisabled() function. It might look something like this:

// hideWhenDisabledAlt :: Node -> Props -> Node
const hideWhenDisabledAlt = el => props => (
    props.isDisabled ? null : el
);

The inside of the function doesn’t change at all. But notice what happens if we partially apply the first parameter now:

import TextField from '@atlaskit/textfield';

// hideWhenDisabledAlt :: Node -> Props -> Node
const hideWhenDisabledAlt = el => props => (
    props.isDisabled ? null : el
);

const newThing = hideWhenDisabled(<TextField name="myinput" id="myinput" />);

What’s the type of newThing ?

That’s right. Since we’ve filled that first Node slot, the type of newThing is \(Props \rightarrow Node\) . The same type as a component. We’ve created a new component that takes just one prop: isDisabled . So, we can say that hideWhenDisabledAlt() is a function that takes a Node and returns a Component.

That’s pretty cool all by itself. But we can take this one step further. What if we could chain together functions like this that returned components? We already have map() which lets us shove a Component into an element enhancer. What if we could do a similar thing and jam components into functions that return components?

As it so happens, this is what the monad definition for functions does. We define a chain() function like so:

// Type signature for chain in general:
// chain :: Monad m => (b -> m c) -> m b -> m c

// Type signature for chain for functions:
// chain :: (b -> Function c) -> Function b -> Function c

// Which becomes:
// chain :: (b -> a -> c) -> (a -> b) -> a -> c
const chain = f => g => x => f(g(x))(x);

Drawn as a diagram, it might look something like this:

RrEbE3Z.png!web

And here’s how it looks inside our Func module:

const Func = {
    map:       f => g => x => f(g(x)),
    contramap: g => f => x => f(g(x)),
    promap:    f => g => h => Func.contramap(f)(Func.map(g)(h)),
    ap:        f => g => x => f(x)(g(x)),
    of:        x => () => x,
    chain:     f => g => x => f(g(x))(x),
    flatMap:   Func.chain,
};

I like to add flatMap() as an alias to chain() . Naming it flatMap() makes more sense and is contsistent with Array.prototype.flatMap() . But, chain() is what we have in the specification. And, to be fair, Brian wrote the Fantasy Land spec before flatMap() for arrays existed.

If we substitute the component type into our diagram above, then it looks like so:

yu67BzN.png!web

What can we do with chain() / flatMap() ? We can take a bunch of functions that return components and chain them together. For example:

import Modal, { ModalTransition } from '@atlaskit/modal-dialog';

// compose :: ((a -> b), (b -> c),  ..., (y -> z)) -> a -> z
const compose = (...fns) => (...args) =>
  fns.reduceRight((res, fn) => [fn.call(null, ...res)], args)[0];

const wrapInModal = inner => ({ onClose, actions, heading }) => (
  <Modal actions={actions} onClose={onClose} heading={heading}>
    {inner}
  </Modal>
);

const showIfOpen = inner => ({ isOpen }) => isOpen && <>{inner}</>;

const withModalTransition = el => <ModalTransition>{el}</ModalTransition>;

const modalify = compose(
  Func.map(withModalTransition),
  Func.chain(showIfOpen),
  Func.chain(wrapInModal),
);

We now have a function modalify() , that will take any Component and place it inside a modal. Not any Element or Node . No, any Component . As a consequence, our new ‘modalified’ component will take four extra props. They are actions , isOpen , onClose and heading . These control the appearance of the modal. But, the way it’s written now, it will pass those to the inner component too. We can prevent that with a prop modifier:

const withoutModalProps = ({ actions, isOpen, onClose, heading, ...props }) =>
  props;

const modalify = compose(
    Func.map(withModalTransition),
    Func.chain(showIfOpen),
    Func.chain(wrapInModal),
    Func.contramap(withoutModalProps),
);

See it in a sandbox

Now, this perhaps isn’t the best example. It will probably be more familiar to most people if we write this out using JSX:

const modalify = Component => ({actions, isOpen, onClose, heading, ...props}) => (
    <ModalTransition>
        {isOpen && (
            <Modal actions={actions} onClose={onClose} heading={heading}>
                <Component {...props} />
            </Modal>
        )}
    </ModalTransition>
);

But why?

Let me ask you a question. We have two versions of the same modalify() function above. One written with composition, the other with plain JSX. Which is more reusable?

It’s a trick question. The answer is neither. They’re the same function. Who cares whether it’s written with composition or JSX? As long as their performance is roughly the same, it doesn’t matter. The important thing is that we can write this function at all . Perhaps you are more clever than I am. But it never would have occurred to me to write a modalify() function before this. Working through the algebraic structure opens up new ways of thinking.

Now, someone might be thinking: “But this is just higher-order components (HOCs). We’ve had those for ages.” And you’d be correct. The React community has been using HOCs for ages. I’m not claiming to introduce anything new here. All I’m suggesting is that this algebraic structure might provide a different perspective.

Most HOCs tend to be similar to our modalify() example. They take a component, modify it, and give you back a new component. But the algebraic structure helps us enumerate all the options. We can:

  1. Modify Nodes (elements) returned from a Component with map() ;
  2. Modify Props going into a Component with contramap() ;
  3. Do both at the same time with promap() ;
  4. Modify Nodes based on values in Props with ap() ; and
  5. Chain together functions that take a Node and return a Component with chain() (aka flatMap() ).

And no, we don’t need promap() or ap() or chain() to do any of these things. But when we do reuse in React, we tend to think only of Components. Everything is a component is the mantra. And that’s fine. But it can also be limiting. Functional programming offers us so many ways of combining functions. Perhaps we could consider reusing functions as well.

Let me be clear. I’m not suggesting anyone go and write all their React components using compose , map() , and chain() . I’m not even suggesting anyone include a Func library in their codebase. What I am hoping is that this gives you some tools to think differently about your React code. I’m also hoping that the algebraic structure of functions makes a little more sense now. This structure is the basis for things like the Reader monad and the State monad. And they’re well worth learning more about.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK