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.

  • 98
Loading comments...

More from Cory Brown