On Composition as the Guiding Principle of Functional Programming
One of the most important concepts in functional programming is composition. If you’ve ever used Unix command line activities to pipe commands together, you have used a form of composition.
One of the most important concepts in functional programming is composition. If you’ve ever used Unix command line activities to pipe commands together, you have used a form of composition.
cat call_log.txt | grep "Texas" | tr [:lower:] [:upper:] > output.txt
We use cat
to get the contents of the file call_log.txt
. We then take that output and feed it right into the grep
command as input. grep
filters the output so that we only have lines containing the text "Texas". Using tr
we accept the output of grep
as our input and output an uppercase version of that text. Finally, we take the output from tr
and we output it to a file named output.txt
.
Each of the commands does one thing very well. You certainly might use any one of those commands on their own. They’re certainly very useful, but when you use them together you unlock real power. By chaining the outputs and inputs of those commands, you can assemble ad hoc, somewhat complex programs with very little effort. This idea of small programs that do one thing well and are made to work together is so prevalent in Unix/Linux culture that it’s called “the Unix philosophy.” At its core, it’s a form of composition very similar to what we do in functional programming.
const isTexas = logData => logData.state === 'Texas'
const sum = (x, y) => x + y
const todaysLogs = [
{ state: 'Texas', recipient: 'Alice', amount: 3.14 },
{ state: 'Florida', recipient: 'Bob', amount: 19.95 },
{ state: 'Texas', recipient: 'Carol', amount: 100.00 }
]
const getTexasAmount = logs =>
logs
.filter(isTexas)
.map(log => log.amount)
.reduce(sum, 0)
const output = getTexasAmount(todaysLogs)
In the example above, we’re composing functions together to do a complex operation. getTexasAmount
takes a single argument—a list of log objects—which we apply a series of functions to. First, we use the function isTexas
with filter
to leave us with just the log records from Texas. Then, we use the output from that function to change our list of logs into a list of Numbers using map
. We finally take that output and use the sum
function in combination with reduce
to add all the amounts together with zero as a starting value, leaving us with a single Number value in output
.
Now, it’s important to note that none of the functions used above actually change the data they’re acting on. filter
doesn’t actually remove any elements from the list. Instead, it returns a whole new list that only has the elements we’re interested in. map
works in a similar fashion, returning a list of Numbers without actually changing the list that was passed into it. reduce
takes a list of items and performs some action on them to combine the elements, but yet again, the original list passed into it was left unchanged. This is also an important concept in functional programming: immutability. Functions that don’t change the underlying data or perform any I/O operations are called “pure functions.” In many ways, they’re like hikers in a national park: They do what they’re there to do, but then they leave no trace that they were ever there. Immutability and pure functions are a big part of why it’s so easy to unit test code written in a functional style. Because they have no persistent state, given the same input, the function will always return the same output; that is to say, they are “deterministic.”
Much like our Unix command example, we have broken a complex problem into smaller pieces and then assembled those pieces together to solve our overall problem. Each of the functions above can be reasoned about individually. If we devise a new, more efficient way to sum two numbers together, we can replace that piece with no issues at all. This is part of why functional programming advocates always talk about how easy it is to refactor an application. Implementation of a function can change as long as it still accepts the same inputs and returns the expected outputs. In addition, we can reuse a lot of these same pieces. If we needed a function to sum together all the log amounts from Florida, we only need to write a new filter function. Everything else can be reused with no changes:
const isFlorida = logData => logData.state === 'Florida'
const getFloridaAmount = logs =>
logs
.filter(isFlorida)
.map(log => log.amount)
.reduce(sum, 0)
One changed line, and we suddenly have a whole new function. The only trouble is that we’re now violating an important principle of good software development: Don’t repeat yourself (sometimes referred to as “DRY”). If the only difference between getFloridaAmount
and getTexasAmount
is one line, why are we repeating those other lines? What if we need to make a change to the logic in the future? Then, we would need to make the change in two places, which opens up the door for bugs. We can avoid this problem by passing our preferred filtering function into our function to get our amounts:
const getAmount = (filterFn, logs) =>
logs
.filter(filterFn)
.map(log => log.amount)
.reduce(sum, 0)
const getTexasAmount = logs => getAmount(isTexas, logs)
const getFloridaAmount = logs => getAmount(isFlorida, logs)
When you pass functions as an argument like this, they’re called higher order functions. Honestly, you’ve probably already used higher order functions if you’re writing JavaScript. When you write a function to go into map
or filter
, that is also using higher order functions. Callback functions, including the ones you put into then
are also higher order functions. Passing functions around as arguments is actually pretty common in Javascript.
The astute among you might have notice that we’re still not DRY. isFlorida
and isTexas
are basically the same function except for the string that we’re checking for, but if we add an argument to it, we can no longer cleanly pass it into filter
since filter takes an element as its argument as it cycles through the list. Currying and partial application of functions are related concepts in functional programming that allow us to get around this problem. Currying a function means setting up your function so that, with each argument, it returns another function that is just “waiting” for the rest of its arguments.
Here’s a DRY version of isTexas
and isFlorida
using traditional JavaScript arguments:
const filterByState = (str, logData) =>
logData.state === str
const isTexas = logData => filterByState('Texas', logData)
const isFlorida = logData => filterByState('Florida', logData)
To be sure, there’s nothing wrong with this approach. It works just fine, and it’s probably how most JavaScript programmers would write the function. We’ve essentially created an “adapter” function that automatically supplies the first argument to our filterByState
function. However, if we use currying to make a function that we can partially apply, we can build an adapter into our filterByState
function itself.
const filterByState = str => logData => logData.state === str
const isTexas = filterByState('Texas')
const isFlorida = filterByState('Florida')
Did you catch what we did there? Let’s walk through what happens in that line where we declare isTexas
above by supplying its argument visually into filterByState
:
const isTexas = logData => logData.state === 'Texas'
When we apply our string to the filterByState
function, it returns another function that takes only one argument, our logData
. By setting up our function in a way where each argument causes the function to return another function that is waiting for the rest of the arguments, we gain the ability to use our function with either one or two arguments supplied. In JavaScript, there is one small syntactic cost to this flexibility. If we want to call our function with both arguments, we need to write it out a little differently:
const onlyFloridaLogs = filterByState('Florida')(todaysLogs)
(We can get creative how we curry our functions to get the best of both worlds, allowing us to call our JavaScript functions with only some arguments while still also allowing us to supply arguments as a comma-separated list. In fact, this is what the Ramda library does with all of its functions. You can certainly write your own function to make other functions curryable, and it can be a great learning excercise. If you’re interested in how to do this, however, I recommend checking out Ramda’s curry function.)
In this example, you might correctly feel like this example is a bit arbitrary. The first “adapter” implementation accomplishes the same thing in a more familiar way for most JavaScript programmers, but I’ll point out the currying approach also lets you get away from needing to store isTexas
and isFlorida
in a separate variable:
const getTexasAmount = logs => getAmount(filterByState('Texas'), logs)
const getFloridaAmount = logs => getAmount(filterByState('Florida'), logs)
Going a step further, if we use currying in our getAmount
implementation, we can remove the need for a parameter from our getTexasAmount
and getFloridaAmount
:
const getAmount = filterFn => logs =>
logs
.filter(filterFn)
.map(log => log.amount)
.reduce(sum, 0)
const getTexasAmount = getAmount(filterByState('Texas'))
const getFloridaAmount = getAmount(filterByState('Florida'))
This is very common way of writing functions in functional programming. It’s called “point-free style.” Advocates of the point-free style say that it leads to more concise and less distracting code that emphasizes the process of transforming your data rather than the data itself. Having played around with functional programming for the last two years, I would say that this style of programming definitely can make you think about your code differently. I have found in my experience that it makes me more likely to think about the ways that I can compose a series of functions together to accomplish a goal, and it also makes it easier for me to see when I might be able to pass a function into another function to accomplish a more abstract and reusable goal. However, I also acknowledge that people are unique and that different tools and techniques will impact folks in varied ways. There’s disagreement in the FP community about whether a point-free style is more or less readable. Regardless of whether you like or dislike point-free style, I would maintain that it’s an important tool to have in your personal toolkit. If you go deeper with functional programming or you work with people who program in a functional style, you will encounter this. I’ve found that learning new coding methodologies is not unlike that moment when an optical illusion shifts in your brain and you suddenly see it in a different way. Understanding how and why point-free style works will give you an entirely different lens through which to look at your code and therefore also new ways to reason about your code.
[Point-free style] helps us think about our functions as generic building blocks that can work with different kinds of data, rather than thinking about them as operations on a particular kind of data. By giving the data a name, we’re anchoring our thoughts about where we can use our functions. By leaving the data argument out, it allows us to be more creative. (Randy Coulman, “Thinking in Ramda: Pointfree Style”)
Taking this same debate a little further, you might be inclined to say that this approach…
const filterByState = (str, logData) =>
logData.state === str
…is clearer than this approach…
const filterByState = str => logData => logData.state === str
I can’t argue with you about that. In a language like JavaScript where you have to set up currying yourself because the default way of doing things is to accept a set list of arguments, the former will always be more familiar, and it’s not wrong. In languages like Haskell or Elm—or even when using a library like Ramda—functions are curried by default, partial application of functions is a more common approach. My biases as an advocate for functional programming make me want to argue that currying is somehow better, but I can’t supply you with hard data to back that up. Frankly, I think getting involved in a debate on it misses the larger and much more important point:
The real magic of what we did above is in reducing the amount of arguments required by the function so that we could successfully add the function to a “chain” or “pipeline” of functions. The technique we used to accomplish that is the much less relevant part. To successfully create a pipeline for your data using pure functions, we ultimately need a set of functions with one input and one output, much like our Unix commands example that we started with:
- A filename ->
cat
-> a stream of the contents in a file - A stream of text ->
grep
-> A filtered stream of text - A stream of text ->
tr
-> A stream of text with some transformations applied - A stream of data -> the
>
operator and a filename -> the stream of text is “assigned” to the file with that name
By plugging in tools that take one input and produce one output, we can link up the output of one tool to the input of another.
- A list of log objects ->
filter
(with our filter function as an argument) -> a filtered list of log objects - A list of log objects ->
map
(with our transformation selecting a single property as an argument) -> A list of numbers - A list of numbers ->
reduce
(with oursum
function and a starting value provided as arguments) -> a single number.
We had to supply some data as arguments to make it so that our functions in the chain only required one input, but this let us create a series of pipes with one input and one output. In doing so, we’ve made interchangeable and reusable pieces. When you construct data pipelines like this, you’re composing functions. In JavaScript where you’re often chaining dotted methods together, it doesn’t look much like when you were composing functions in algebra, but if we use some helper functions from Ramda, it’s a little more apparent:
const { filter, map, reduce } = require('ramda')
const getAmount = filterFn => logs =>
reduce(sum, 0)(map(log => log.amount)(filter(filterFn, logs)))
Not unlike when your algebra teacher would write f (g (h (x)))
on the board if you squint just right.
With Ramda’s compose
function, we can arguably make that function look a little cleaner by leveraging currying and partial application of functions:
const { filter, map, reduce, compose } = require('ramda')
const getAmount = filterFn => compose (
reduce(sum, 0),
map(log => log.amount),
filter(filterFn)
)
Or if you prefer pipe
so that the order of our functions is more sequential like when we used the dotted methods above:
const { filter, map, reduce, pipe } = require('ramda')
const getAmount = filterFn => pipe (
filter(filterFn),
map(log => log.amount),
reduce(sum, 0)
)
In these last two code examples, we’re still waiting for a list of log objects. Because the functions in Ramda are curried, when we don’t have all of the arguments a function needs in order to run, it returns a function that politely waits for the next argument. The fact that we have a generic function that won’t actually run until it’s supplied with that last argument is actually a very powerful idea. Regardless of what this newly composed chain of functions actually does in the end, it won’t actually do any of those things until we supply that last argument. This means that we can pass that function around as a higher-order function. Maybe we have a list of lists of log objects—perhaps we have a different log object for each day?—and we want to do get the total amount for Texas for each list. We could pass getAmount(filterByState('Texas'))
into map
for that list of lists. Maybe we’re writing a library, and we don’t yet know what data our users are going to want to supply to this function. In that case, just export out this composed function from our module and let our users call the function as needed. Maybe we need to run this transformation on the result of an asynchronous network call. We can pass this function into then
.
As I’ve mentioned above, there’s no inherent magic to currying functions. You can accomplish these same things by writing “adapter” functions that reduce the arity of the function (the number of arguments that it takes) to one. The real magic lies of creating a series of functions that take one input and produce one output. Currying just lets us be a little more lazy in how we get there.
Because this style of programming is not usually what we’re taught in our CS classes or at our first jobs, some of the techniques I’ve used above might feel strange or foreign. That’s a perfectly valid way to feel. You might even feel like the code isn’t particularly readable. Hell, I would agree with you in some places. So much so that I would actually like to rewrite that code using functional techniques in a way that I would consider more readable, just for the sake of completeness:
const { filter, map, reduce, pipe } = require('ramda')
const todaysLogs = [
{ state: 'Texas', recipient: 'Alice', amount: 3.14 },
{ state: 'Florida', recipient: 'Bob', amount: 19.95 },
{ state: 'Texas', recipient: 'Carol', amount: 100.0 }
]
const makeFilterByState = str => logData => logData.state === str
const filterByState = state => filter(makeFilterByState(state))
const sum = (x, y) => x + y
const sumAll = reduce(sum, 0)
const logToAmount = obj => obj.amount
const getFilteredTotal = state =>
pipe(
filterByState(state),
map(logToAmount),
sumAll
)
const getTexasTotal = getFilteredTotal('Texas')
const getFloridaTotal = getFilteredTotal('Florida')
module.exports = {
makeFilterByState,
filterByState,
sum,
sumAll,
logToAmount,
getFilteredTotal,
getTexasTotal,
getFloridaTotal
}
console.log(getTexasTotal(todaysLogs))
console.log(getFloridaTotal(todaysLogs))
When evaluating readability, I suspect it’s always going to be at least somewhat subjective based on the reviewer, but I would make the argument that getFilteredToday
reads at least somewhat like we might sound like in explaining what the code does: “First, it filters by the state we’re given as an argument. Then, it maps each object in our list to just the Amount property, and finally, it sums up all the amounts.” That feels like a readability win for me. Coming back into this code months later, I’m confident that I’ll understand what each of those pieces does without much ramp up time—especially if I’ve taken the time (as I should) to write unit tests for each of those functions demonstrating how they’re expected to be used:
const {
makeFilterByState,
filterByState,
sum,
sumAll,
logToAmount,
getFilteredTotal,
getTexasTotal,
getFloridaTotal
} = require('./logs')
const sample = [
{ state: 'Texas', recipient: 'Alice', amount: 3.14 },
{ state: 'Florida', recipient: 'Bob', amount: 19.95 },
{ state: 'Texas', recipient: 'Carol', amount: 100.0 }
]
describe('makeFilterByState', () => {
it('Returns true if state property matches.', () => {
const val = { state: 'Texas' }
expect(makeFilterByState('Texas')(val)).toEqual(true)
})
it('Returns false if state property does not match', () => {
const val = { state: 'Texas' }
expect(makeFilterByState('Florida')(val)).toEqual(false)
})
})
describe('filterByState', () => {
it('Returns only values that match supplied state.', () => {
const expected = [{ state: 'Florida', recipient: 'Bob', amount: 19.95 }]
expect(filterByState('Florida')(sample)).toEqual(expected)
})
})
describe('sum', () => {
it('adds two numbers together.', () => {
expect(sum(1, 2)).toBe(3)
})
})
describe('sumAll', () => {
it('adds a list of numbers together.', () => {
expect(sumAll([1, 2, 3, 4, 5])).toBe(15)
})
})
describe('logToAmount', () => {
it('Returns the amount property of an object', () => {
expect(logToAmount(sample[0])).toBe(3.14)
})
})
describe('getFilteredTotal', () => {
it('Returns the sum of all logs matching the provided state', () => {
expect(getFilteredTotal('Texas')(sample)).toBe(103.14)
})
})
describe('getTexasTotal', () => {
it('Returns the sum of all logs from Texas.', () => {
expect(getTexasTotal(sample)).toBe(103.14)
})
})
describe('getFloridaTotal', () => {
it('Returns the sum of all logs from Florida.', () => {
expect(getFloridaTotal(sample)).toBe(19.95)
})
})
If I come back into this code—or if one of my co-workers does—this approach helps us see both (1) what these functions are doing, as well as (2) where we can extend the functionality if we need to add new code. Suppose we received a new requirement wherein we had to add a property to our data that indicated whether or not the log entry was reportable, and we were not allowed to count non-reportable data in our totals:
// Assume that all other code is unchanged
// MODULE
const todaysLogs = [
{ state: 'Texas', recipient: 'Alice', amount: 3.14, reportable: true },
{ state: 'Florida', recipient: 'Bob', amount: 19.95, reportable: true },
{ state: 'Texas', recipient: 'Carol', amount: 100.00, reportable: false }
]
// Because JavaScript doesn’t include strict typing and forces non-boolean
// values into either “truthy” or “false-y”, I usually prefer to
// explicitly check Boolean values. This is a matter or preference, of
// course, but still something I might mention in a code review.
// Less surface area for bugs is a good thing IMO.
const filterOutNonReportable = filter(log => log.reportable === true)
const getFilteredTotal = state => pipe(
filterOutNonReportable,
filterByState(state),
map(onlyAmountProperty),
sumAll
)
// TESTS
const sample = [
{ state: 'Texas', recipient: 'Alice', amount: 3.14, reportable: true },
{ state: 'Florida', recipient: 'Bob', amount: 19.95, reportable: true },
{ state: 'Texas', recipient: 'Carol', amount: 100.00, reportable: false }
]
describe('filterByState', () => {
it('Returns only values that match supplied state.', () => {
const expected = [{ state: 'Florida', recipient: 'Bob', amount: 19.95, reportable: true }]
expect(filterByState('Florida')(sample)).toEqual(expected)
})
})
describe('getFilteredTotal', () => {
it('Returns the sum of all logs matching the provided state', () => {
expect(getFilteredTotal('Texas')(sample)).toBe(3.14)
})
})
describe('getTexasTotal', () => {
it('Returns the sum of all logs from Texas.', () => {
expect(getTexasTotal(sample)).toBe(3.14)
})
Good code should be readable. It should be easily extensible. It should also be testable to confirm that it actually does what you intend at every step. Functional programming makes all of these things extremely easy—so much so that fans of the functional style usually see them as necessary by-products of writing code this way (and therefore do a bad job of actually making the case for why FP could help developers actually write better code). Because most of us first learned about programming in the context of an imperative style with either object-oriented or procedural techniques, some of the specific techniques we use in FP might feel a little unfamiliar at first, but it’s important to remember that you shouldn’t let that unfamiliarity get you down. If you feel stuck, go back to the overall goal of functional programming and ask yourself: How does this technique help me compose functions together? Because that is the underlying question that drives functional programming as a coding style.