RSS

Ramda Chops: Function Composition

Info

SummaryLearn about function composition with ramda's compose function.
Shared2018-01-26
Revised2023-02-12 @ 18:00 UTC

Thanks to @evilsoft and @zerkms for their review of this post.

Composition is defined as “the combining of distinct parts or elements to form a whole.” source If we apply this thinking to functions in programming, then function composition can be seen as the combining of functions to form a new function that is composed of said functions. Now that that word salad is over, let’s get to work.

We have a task, and our task is to write a function that 1. accepts a list of objects containing score (Number) and name (String) properties 1. returns the top 3 scorers’ names from highest to lowest

Here are the unordered results that we have to work with:

const results = [
  { score: 40, name: 'Aragorn' },
  { score: 99, name: 'Bilbo' },
  { score: 63, name: 'Celeborn' },
  { score: 77, name: 'Denethor' },
  { score: 100, name: 'Eowin' },
  { score: 94, name: 'Frodo' }
]

Other ramda posts:

First Approach

// getHighScorers :: [Object] -> [String]
const getHighScorers = xs =>
  [...xs]
    .sort((a, b) => b.score - a.score)
    .slice(0, 3)
    .map(x => x.name)

getHighScorers(results) // => [ 'Eowin', 'Bilbo', 'Frodo' ]

As cautious JavaScript developers, we know to reach for our functions and methods that don’t mutate the objects we’re receiving. We use a copy of the original list and chain together operations that sort, slice and map the return values of each operation until we arrive at [ 'Eowin', 'Bilbo', 'Frodo' ]. Many folks would stop here, write a few unit tests and be done with it. We, on the other hand, will take this to the next level.

Extracting Reusable Functions

Our getHighScorers function has some functionality that we may want to use elsewhere in the future. Let’s break down what we might be able to extract:

// Altered slightly to allows us to compare
// things like strings and numbers.
//
// descBy :: (String, [a]) -> [a]
const descBy = (prop, xs) =>
  [...xs].sort((a, b) =>
    a[prop] < b[prop] ? 1 : (a[prop] === b[prop] ? 0 : -1)
  )

// takeN :: (Number, [a]) -> [a]
const takeN = (n, xs) =>
  xs.slice(0, n)

// mapProp :: (String, [a]) -> [b]
const mapProp = (prop, xs) =>
  xs.map(x => x[prop])

// 1. pass `score` and `xs` to `descBy`
// 2. pass the return value of `descBy`
//    to `takeN(3, __)`
// 3. pass the return value of `takeN`
//    to `mapProp('name', __)` where we map over
//    the list and pull out each one's `name`
//
// getHighScorers :: [Object] -> [String]
const getHighScorers = xs =>
  mapProp('name', takeN(3, descBy('score', xs)))

// results object here...

getHighScorers(results) // => [ 'Eowin', 'Bilbo', 'Frodo' ]

Try this code in the ramda REPL.

This is starting to look good, but that getHighScorers function is looking a bit dense. Since we have a seeming pipeline of transformations that we’re applying to a list, wouldn’t it be great if we could simply list these transformations in a “flat” way (instead of a “nested” way like we do above) and then pass the data to this list of transformations?

Enter compose

Let’s take our getHighScorers function and rewrite it using ramda’s compose function:

import compose from 'ramda/src/compose'

// const getHighScorers = xs =>
//   mapProp('name', takeN(3, descBy('score', xs)))

// getHighScorers :: [Object] -> [String]
const getHighScorers = xs =>
  compose(mapProp('name'), takeN(3), descBy('score'))(xs)

Let’s first clarify what compose is doing:

compose(f, g)(x) === f(g(x))

Say it aloud: “f after g.” With compose, the function furthest to the right is applied first with the value (x), and the return value of that function is passed to the next function to its left, and repeat this until all functions have been applied.

Cool – but wait! How can descBy, takeN and mapProp only accept one argument at a time when they all accept two?! In order to make these a reality, we can make use of ramda’s curry function which we dove into in my previous post on function currying.

import compose from 'ramda/src/compose'
import curry from 'ramda/src/curry'

// descBy :: String -> [a] -> [a]
const descBy = curry((prop, xs) =>
  [...xs].sort((a, b) =>
    a[prop] < b[prop] ? 1 : (a[prop] === b[prop] ? 0 : -1)
  )
)

// takeN :: Number -> [a] -> [a]
const takeN = curry((n, xs) =>
  xs.slice(0, n)
)

// mapProp :: String -> [a] -> [b]
const mapProp = curry((prop, xs) =>
  xs.map(x => x[prop])
)

// getHighScorers :: [Object] -> [String]
const getHighScorers =
  compose(mapProp('name'), takeN(3), descBy('score'))

Try this code in the ramda REPL.

You may also notice that we removed xs => from getHighScorers because when we use compose and pass the final argument in at the end, it in fact becomes redundant. Our composition sits and waits for either the data to be applied or for it to be used another way: more compositions! This leads us down a powerful path whereby we can now compose different functions together and combine them into a final composition.

Composing a Composition

// getTop3 :: [a] -> [a]
const getTop3 =
  compose(takeN(3), descBy('score'))

// getHighScorers :: [Object] -> [String]
const getHighScorers =
  compose(mapProp('name'), getTop3)

Try this code in the ramda REPL.

This is where we truly begin to see the power of compose, for we are able to break our functions or function compositions out into tiny little pieces that we chain together like water pipes or guitar pedals.

Guitar pedals lined up side-by-side
_Guitar pedals by [Henrik Hjortshøj](https://unsplash.com/@hfranke)_

We are now empowered (nay – encouraged!) to provide meaningful names in the context of what we’re trying to accomplish.

Composing compositions also allows us to use our type signatures to tell a story about what behavior is expected with each little part on our path to the ultimate goal.

pipe vs compose

For various reasons that are usally a matter of opinion, many people prefer function application to flow from left to right instead of right to left (the latter being what you get with compose). So if you find yourself thinking the same thing, pipe is for you:

//      <-------------   <------   <-------------
compose(mapProp('name'), takeN(3), descBy('score'))(xs)

// versus

//   -------------->  ------->  -------------->
pipe(descBy('score'), takeN(3), mapProp('name'))(xs)

Composing Promises

There’s really nothing to it! Instead of compose or pipe, use composeP or pipeP.

Debugging

Once you adopt this pattern, you may find it initially difficult to inspect your data at a given point in the pipeline; however, here’s a tip that will solve most of your problems:

compose(
  mapProp('name'),
  x => (console.log(x), x),
  takeN(3),
  descBy('score')
)

// or

const logIt = x => (console.log(x), x)

compose(
  mapProp('name'),
  logIt,
  takeN(3),
  descBy('score')
)

This logs whatever the value in the pipeline is at that time and returns that value to pass it on just as it would have.


Thanks for reading! Until next time,
Robert