DEV Community

Jethro Larson
Jethro Larson

Posted on • Edited on

Beautiful Functions: Compose

I'd like to take a look at some functions whose form and function are the epitome of elegant.

The B combinator, sometimes called "compose":

const B = (f) => (g) => (x) => f(g(x))
Enter fullscreen mode Exit fullscreen mode

Which has the type:
(b -> c) → (a -> b) → a → c

in TypeScript:

const B = <A, B, C>(g: (y: B) => C) =>
  (f: (x: A) => B) =>
    (a: A): C =>
      g(f(a));
Enter fullscreen mode Exit fullscreen mode

What it does is combine two unary functions (single-argument functions) together such that the output of the second is the input of the first. This is the core of composing functions in math as well as programming. If you have a couple procedures you want to link and sequence consider using this operator to do so.

const armTheMissiles = (missles: Missle[]): Missle[] => {...}

const fireTheMissles = (missles: Missle[]): void => {...}

const armAndFireMissles = B(fireTheMissles)(armTheMissles)
Enter fullscreen mode Exit fullscreen mode

This is such a core way of writing code that advanced languages like Haskell and F# have operators dedicated to it: . and <<, respectively.

armAndFireMissles = fireTheMissles . armTheMissles
Enter fullscreen mode Exit fullscreen mode

An easy place to integrate this function into your code is in cases where you take a callback that calls a function with it's parameters.

const fetchStuff = () =>
  fetch(...).then(data => parseData(validate(data)))
Enter fullscreen mode Exit fullscreen mode

In this case you can use the B combinator to drop the inner lambda:

const fetchStuff = () =>
  fetch(...).then(B(parseData)(validate))
Enter fullscreen mode Exit fullscreen mode

This way of eliminating lambdas by using composition techniques is called eta-reduction.

It may help to have an idea of what a partially applied B means. I like to think of it as the function has its arms out and is ready for the conga line.

const congaReadyFoo = B(foo);
const congaReadyBar = B(bar);
const congaLine = congaReadyFoo(congaReadyBar(baz));
// where foo, bar, and baz are all unary functions with compatible inputs and outputs.
Enter fullscreen mode Exit fullscreen mode

That all said, it's easy to go too far with this kind of technique.

// probably too far
const congaLine = B(foo)(B(bar)(baz))
Enter fullscreen mode Exit fullscreen mode

This is mainly due to the syntax as when it's an operator it's much easier to follow (haskell):

congaLine = foo . bar . baz 
Enter fullscreen mode Exit fullscreen mode

There's many more fun combinators but I wanted to start with one many people may already be familiar with from high school math.

Bonus fact: B turns functions themselves into a functor

class Functor f where
  fmap :: (a -> b) -> f a -> f b

instance Functor ((->) r) where
  fmap = (.)
Enter fullscreen mode Exit fullscreen mode

That is, given a function from a -> b, our compose function takes a function which takes something of type a and returns a function which takes something of type b. This means it's totally cool to think of composition as mapping a function over another function.

Top comments (2)

Collapse
 
pcjmfranken profile image
Peter Franken

Very good explanation!

I never really understood or even cared about the concept until I saw it used in production (all those wasted years!). If you have time, maybe add a real-world practical use example for stragglers like me 😉

Collapse
 
jethrolarson profile image
Jethro Larson • Edited

I showed one which is

const fetchStuff = () =>
  fetch(...).then(data => parseData(validate(data))
Enter fullscreen mode Exit fullscreen mode

to

const fetchStuff = () =>
  fetch(...).then(B(parseData)(validate))
Enter fullscreen mode Exit fullscreen mode