Why would anyone do that?
const identity = (context) => context
const map = (mappingFn) => (context = []) =>
context.map(mappingFn)
const alwaysTrue = (...contexts) => true
const alwaysFalse = (...contexts) => false
Function Composition
The counter-intuative patterns that will simplify your code, help you ship fast, be more stable, and grow a thicker, fuller, healthier hair.
Function DeComposition
Function Factoring
Function
Composition
an act or mechanism to combine simple functions to build more complicated ones
https://en.wikipedia.org/wiki/Function_composition_(computer_science)
const doesStuff = (context) =>
{ ... }
const doesOtherStuff = (context) =>
{ ... }
const whyNotBoth = (context) =>
doesStuff(doesOtherStuff(context))
// We use function composition
// in React a lot
connect(...)(withRouter(Component))
Function
Factoring
the process of breaking a complex problem or system into parts that are easier to conceive, understand, program, and maintain
https://en.wikipedia.org/wiki/Decomposition_(computer_science)
const doManyThings = (context) => {
const clone = { ...context }
clone.foo = 'bar'
console.log(clone)
if (!clone.isValid) {
throw new Error('Not Valid')
}
return clone
}
const doManyThings =
assert(
({ isValid }) => isValid,
throw new Error('Not Valid')
)(
log(
assign({ foo: 'bar' })(
clone(
context
)
)
)
)
You're
Not
Helping!
Why don't you just tell me what you want?
Rather than spelling out every step of how to do something, how about we just declare what, and not worry about how each what gets accomplished, at least for now?
const doManyThings = (context) =>
context ->
clone ->
assign({ foo: 'bar' }) ->
log ->
assert(
({ isValid }) => isValid,
throw new Error('Not Valid')
)
const doManyThings = (context) =>
context |
clone |
assign({ foo: 'bar' }) |
log |
assert(
({ isValid }) => isValid,
throw new Error('Not Valid')
)
Pipe Dream
I spent much of my 7th grade building rube-goldberg-esque pipe layouts on windows 3.1 😱
Â
Now I spend much of my professional time building data pipelines.
Â
Some kids never grow up.
Composed functions are evaluated inside out
the innermost value is passed to the innermost function. It's return valued is passed as the argument to the next innermost function, etc.
const doManyThings =
assert(
({ isValid }) => isValid,
throw new Error('Not Valid')
)(
log(
assign({ foo: 'bar' })(
clone(
context
)
)
)
)
const doManyThings = (context) =>
assert({ isValid }) => isValid,
throw new Error('Not Valid')
) <-
log <-
assign({ foo: 'bar' }) <-
clone <-
context
const doManyThings = (x) =>
f(g(h(i(x))))
const doManyThings = (x) =>
f <- g <- h <- i <- x
Compose
It's not a pipe (see what I did there?)
What is this voodoo magic?
many libraries include a compose function including redux, lodash, underscore, ramda, recompose, ...
Â
It helps to see an implementation to understand it.
const compose = (...fns) => (context) =>
fns.reduceRight((prevVal, nextFn =>
nextFn(prevVal),
context
)
Pipe: Compose it forward
const pipe = (...fns) => (context) =>
fns.reduce((prevVal, nextFn =>
nextFn(prevVal),
context
)
Which brings us back to doh!
Function composition is cool and all, but we haven't really answered why someone would feel the need to rewrite a built in function like map or filter when they're already on the array prototype.
const identity = (context) => context
const map = (mappingFn) => (context) =>
context.map(mappingFn)
const filter = (filterFn) => (context) =>
context.filter(filterFn)
Context is everything
Function composition's strengths lies in its constraints.
Â
- Functions can only take one argument.
- Functions can only return one value.
- The value returned from the last function becomes the argument for the next function.
Context is everything
The context is the subject of the function. The thing upon which the function is operating. The data.
array.map((item) => { ... })
array.filter((item) => { ... })
Context is everything
The context is what is passed through the pipeline. Therefore, it needs to be an argument.
(array) => array.map((item) => { ... })
(array) => array.filter((item) => { ... })
More?
What if you need more than just the context -- if you need to configure your function?
const map = (array, mappingFn) =>
array.map(mappingFn)
const map = (mappingFn, array) =>
array.map(mappingFn)
const map = (mappingFn) => (array) =>
array.map(mappingFn)
To make the function composable, have the function return after accepting everything except the context. Have it return a function that then only accepts the context.
You still have access to those config arguments inside the closure
Make the context the last argument accepted
(array) => array.map((item) => { ... })
You haven't
answered
the question
Indeed
Why would anyone do that?
const identity = (context) => context
const map = (mappingFn) => (context = []) =>
context.map(mappingFn)
const alwaysTrue = (...contexts) => true
const alwaysFalse = (...contexts) => false
Function factoring leads to generalizations.
Generalizations facilitate small, well tested, oft used functions
Generalizations are predictable.
Generalizations are, ideally, pure.
Good generalizations mean most of your code is just a composition of existing, tested functions
Why would anyone do that?
const identity = (context) => context
const map = (mappingFn) => (context = []) =>
context.map(mappingFn)
const alwaysTrue = (...contexts) => true
const alwaysFalse = (...contexts) => false
What if I told you you already were?
<ThemeProvider theme={theme}>
<BrowserRouter>
<Main>
<Nav>
<NavItem>
<a to='/home'>
<span>Home</span>
</a>
</NavItem>
</Nav>
</Main>
</BrowserRouter>
</ThemeProvider>
React.createElement(
ThemeProvider, { theme: theme }, React.createElement(
BrowserRouter, null, React.createElement(
Main, null, React.createElement(
Nav, null, React.createElement(
NavItem, null, React.createElement(
'a', { to: '/home' }, React.createElement(
'span', null, 'Home'
)
)
)
)
)
)
)
pipe(
React.createElement('a', { to: '/home' }),
React.createElement('span', null),
React.createElement(NavItem, null),
React.createElement(Nav, null),
React.createElement(Main, null),
React.createElement(BrowserRouter, null),
React.createElement(ThemeProvider, { theme: theme })
)('Home')
compose(
React.createElement(ThemeProvider, { theme: theme })
React.createElement(BrowserRouter, null),
React.createElement(Main, null),
React.createElement(Nav, null),
React.createElement(NavItem, null),
React.createElement('span', null),
React.createElement('a', { to: '/home' }),
)('Home')
React
app.use((req, res, next) => {
...
next()
})
app.use(someSpecializedThing)
app.use(otherSpecializedThing)
app.use(nextSpecializedThing)
app.use(sendSomethingBack)
Express
someSpecializedThing ->
otherSpecializedThing ->
nextSpecializedThing ->
sendSomethingBack
pipe(
someSpecializedThing,
otherSpecializedThing,
nextSpecializedThing,
sendSomethingBack
)([req, res])
somePromiseWhichIsAFutureValue
.then(somethinWithThatValue)
.then(somethinWithThePreviousValue)
.then(somethinWithTheValueJustBeforeThis)
Promises
// If we pretend `pipe` is async
pipe(
somethinWithThatValue,
somethinWithThePreviousValue,
somethinWithTheValueJustBeforeThis
)(somePromiseWhichIsAFutureValue)
Like the best friend you never knew was there.
Whether you have realized it or not, you have been using function composition from the beginning. Imagine what you can do now that you recognize the pattern and can implement it in the code you write.
- Small (single responsibility) units of code
- Easier to test
- Greater reuse of code
- Greater confidence and predictability
- Declarative (what, not how)
- Smaller surface area for bugs
- Fewer context switches when reading
- Retains the same abstraction level more consistently
- Easier to reason about
- Has some 50 years of battle testing
Advantages
- Learning curve for you
- Learning curve for the next dev
- Is theoretically less performant*
- Different from or obscured by most JavaScript you see
Disadvantages
Why would anyone do that?
By Cory Brown
Why would anyone do that?
The counter-intuative patterns of function composition that will simplify your code, help you ship fast, be more stable, and grow a thicker, fuller, healthier hair.
- 978