The Power of Functions Returning Other Functions in JavaScript

jsmanifest

jsmanifest

Posted on June 10, 2020

The Power of Functions Returning Other Functions in JavaScript

Find me on medium

JavaScript is widely known for being extremely flexible by its nature. This post will show some examples of taking advantage of this by working with functions.

Since functions can be passed around anywhere, we can pass them into the arguments of functions.

My first hands-on experience with anything having to do with programming in general was getting started with writing code in JavaScript, and one concept in practice that was confusing to me was passing functions into other functions. I tried to do some of this "advanced" stuff that all the pros were doing but I kept ending up with something like this:

function getDate(callback) {
  return callback(new Date())
}

function start(callback) {
  return getDate(callback)
}

start(function (date) {
  console.log(`Todays date: ${date}`)
})
Enter fullscreen mode Exit fullscreen mode

This was absolutely ridiculous, and even made it more difficult to understand why we would even pass functions into other functions in the real world, when we could have just done this and get the same behavior back:

const date = new Date()
console.log(`Todays date: ${date}`)
Enter fullscreen mode Exit fullscreen mode

But why isn't this good enough for more complex situations? What is the point of creating a custom getDate(callback) function and having to do extra work, besides feeling cool?

I then proceeded to ask about more questions about these use cases and asked to be given an example of a good use on a community board, but no one wanted to explain and give an example.

Thinking back from now, I realized that the problem was that my mind did not know how to think programmatically yet. It takes awhile to get your mind shifted from your original life towards programming in a computer language.

Since I understand the frustrations of trying to understand when higher order functions are useful in JavaScript, I decided to write this article to explain step by step on a good use case starting from a very basic function which anyone can write, and work our way up from there into a complex implementation that provides additional benefits.

The function with intention

First we will start with a function that is intended to achieve a goal for us.

How about a function that will take an object and return a new object that updated the styles the way we wanted it to?

Lets work with this object (we will reference this as a component):

const component = {
  type: 'label',
  style: {
    height: 250,
    fontSize: 14,
    fontWeight: 'bold',
    textAlign: 'center',
  },
}
Enter fullscreen mode Exit fullscreen mode

We want to make our function keep the height no less than 300 and apply a border to button components (components with type: 'button') and return it back to us.

This can look something like this:

function start(component) {
  // Restrict it from displaying in a smaller size
  if (component.style.height < 300) {
    component.style['height'] = 300
  }
  if (component.type === 'button') {
    // Give all button components a dashed teal border
    component.style['border'] = '1px dashed teal'
  }
  if (component.type === 'input') {
    if (component.inputType === 'email') {
      // Uppercase every letter for email inputs
      component.style.textTransform = 'uppercase'
    }
  }
  return component
}

const component = {
  type: 'div',
  style: {
    height: 250,
    fontSize: 14,
    fontWeight: 'bold',
    textAlign: 'center',
  },
}

const result = start(component)
console.log(result)
Enter fullscreen mode Exit fullscreen mode

Result:

{
  "type": "div",
  "style": {
    "height": 300,
    "fontSize": 14,
    "fontWeight": "bold",
    "textAlign": "center"
  }
}
Enter fullscreen mode Exit fullscreen mode

Lets pretend we came up with an idea that each component can have more components inside of it by placing them inside its children property. That means we have to make this handle the inner components as well.

So, given a component like this:

{
  "type": "div",
  "style": {
    "height": 300,
    "fontSize": 14,
    "fontWeight": "bold",
    "textAlign": "center"
  },
  "children": [
    {
      "type": "input",
      "inputType": "email",
      "placeholder": "Enter your email",
      "style": {
        "border": "1px solid magenta",
        "textTransform": "uppercase"
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Our function obviously isn't capable of getting the job done, yet:

function start(component) {
  // Restrict it from displaying in a smaller size
  if (component.style.height < 300) {
    component.style['height'] = 300
  }
  if (component.type === 'button') {
    // Give all button components a dashed teal border
    component.style['border'] = '1px dashed teal'
  }
  if (component.type === 'input') {
    if (component.inputType === 'email') {
      // Uppercase every letter for email inputs
      component.style.textTransform = 'uppercase'
    }
  }
  return component
}
Enter fullscreen mode Exit fullscreen mode

Since we recently added in the concept of children to components, we now know there are at least two different things going on to resolve the final result. This is a good time to start thinking about abstraction. Abstracting away pieces of code into reusable functions makes your code more readable and maintainable because it prevents troublesome situations like debugging some issue in the implementation details of something.

When we abstract small parts away from something it's also a good idea to begin thinking about how to put these pieces together later, which we can refer to as composition.

Abstraction and Composition

To know what to abstract away, think about what our end goal was:

"A function that will take an object and return a new object that updated the styles on it the way we want it to"

Essentially the whole point of this function is to transform a value to be in the representation we expect it to. Remember that our original function was transforming styles of a component but then we also added in that components could also contain components within themselves by its children property, so we can start with abstracting those two parts away since there's a good chance there will most likely be more situations where we need to make more functions that need to do similar things to the value. For the sake of this tutorial can refer to these abstracted functions as resolvers:

function resolveStyles(component) {
  // Restrict it from displaying in a smaller size
  if (component.style.height < 300) {
    component.style['height'] = 300
  }
  if (component.type === 'button') {
    // Give all button components a dashed teal border
    component.style['border'] = '1px dashed teal'
  }
  if (component.type === 'input') {
    if (component.inputType === 'email') {
      // Uppercase every letter for email inputs
      component.style.textTransform = 'uppercase'
    }
  }
  return component
}

function resolveChildren(component) {
  if (Array.isArray(component.children)) {
    component.children = component.children.map((child) => {
      // resolveStyles's return value is a component, so we can use the return value from resolveStyles to be the the result for child components
      return resolveStyles(child)
    })
  }
  return component
}

function start(component, resolvers = []) {
  return resolvers.reduce((acc, resolve) => {
    return resolve(acc)
  }, component)
}

const component = {
  type: 'div',
  style: {
    height: 250,
    fontSize: 14,
    fontWeight: 'bold',
    textAlign: 'center',
  },
  children: [
    {
      type: 'input',
      inputType: 'email',
      placeholder: 'Enter your email',
      style: {
        border: '1px solid magenta',
      },
    },
  ],
}

const result = start(component, [resolveStyles, resolveChildren])
console.log(result)
Enter fullscreen mode Exit fullscreen mode

Result:

{
  "type": "div",
  "style": {
    "height": 300,
    "fontSize": 14,
    "fontWeight": "bold",
    "textAlign": "center"
  },
  "children": [
    {
      "type": "input",
      "inputType": "email",
      "placeholder": "Enter your email",
      "style": {
        "border": "1px solid magenta",
        "textTransform": "uppercase"
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Breaking changes

Next lets talk about how this code can cause catastrophic errors--errors that will crash your app.

If we take a close look at the resolvers and look at how they're being used to compute the final result, we can tell that it can easily break and cause our app to crash because of two reasons:

  1. It mutates - What if an unknown bug were to occur and mutated the value incorrectly by mistakenly assigning undefined values to the value? The value also fluctuates outside the function because it was mutated (understand how references work).

If we take out return component from resolveStyles, we're immediately confronted with a TypeError because this becomes the incoming value for the next resolver function:

TypeError: Cannot read property 'children' of undefined
Enter fullscreen mode Exit fullscreen mode
  1. Resolvers override previous results - This is not a good practice and defeats the purpose of abstraction. Our resolveStyles can compute its values but it won't matter if the resolveChildren function returns an entirely new value.

Keeping things immutable

We can safely move towards our goal by making these functions immutable and ensure that they always return the same result if given the same value.

Merging new changes

Inside our resolveStyles function we could return a new value (object) containing the changed values that we will merge along with the original value. This way we can ensure that resolvers do not override eachother, and returning undefined will take no effect for other code afterwards:

function resolveStyles(component) {
  let result = {}

  // Restrict it from displaying in a smaller size
  if (component.style.height < 300) {
    result['height'] = 300
  }
  if (component.type === 'button') {
    // Give all button components a dashed teal border
    result['border'] = '1px dashed teal'
  }
  if (component.type === 'input') {
    if (component.inputType === 'email') {
      // Uppercase every letter for email inputs
      result['textTransform'] = 'uppercase'
    }
  }
  return result
}

function resolveChildren(component) {
  if (Array.isArray(component.children)) {
    return {
      children: component.children.map((child) => {
        return resolveStyles(child)
      }),
    }
  }
}

function start(component, resolvers = []) {
  return resolvers.reduce((acc, resolve) => {
    return resolve(acc)
  }, component)
}
Enter fullscreen mode Exit fullscreen mode

When a project becomes bigger

If we had 10 style resolvers and only 1 resolver working on children, it can become difficult to maintain so we can split them up in the part where they become merged:

function callResolvers(component, resolvers) {
  let result

  for (let index = 0; index < resolvers.length; index++) {
    const resolver = resolvers[index]
    const resolved = resolver(component)
    if (resolved) {
      result = { ...result, ...resolved }
    }
  }

  return result
}


function start(component, resolvers = []) {
  let baseResolvers
  let styleResolvers

  // Ensure base resolvers is the correct data type
  if (Array.isArray(resolvers.base)) baseResolvers = resolvers.base
  else baseResolvers = [resolvers.base]
  // Ensure style resolvers is the correct data type
  if (Array.isArray(resolvers.styles)) styleResolvers = resolvers.styles
  else styleResolvers = [resolvers.styles]

  return {
    ...component,
    ...callResolvers(component, baseResolvers),
    style: {
      ...component.style,
      ...callResolvers(component, styleResolvers)),
    },
  }
}
Enter fullscreen mode Exit fullscreen mode

The code that calls these resolvers have been abstracted out into its own function so we can reuse it and also to reduce duplication.

What if we have a resolvers that need some more context to compute its result?

For example, what if we have a resolveTimestampInjection resolver function that injects a time property when some options parameter was used passed somewhere in the wrapper?

Functions needing additional context

It would be nice to give resolvers the ability to get additional context and not just receive the component value as an argument. We can provide this ability with using the second parameter of our resolver functions, but I think those parameters should be saved for lower level abstractions on a component level basis.

What if resolvers had the ability to return a function and receive the context they need from the returned function's arguments instead?

Something that looks like this:

function resolveTimestampInjection(component) {
  return function ({ displayTimestamp }) {
    if (displayTimestamp === true) {
      return {
        time: new Date(currentDate).toLocaleTimeString(),
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

It would be nice if we can enable this functionality without changing the behavior of the original code:

function callResolvers(component, resolvers) {
  let result

  for (let index = 0; index < resolvers.length; index++) {
    const resolver = resolvers[index]
    const resolved = resolver(component)
    if (resolved) {
      result = { ...result, ...resolved }
    }
  }

  return result
}


function start(component, resolvers = []) {
  let baseResolvers
  let styleResolvers

  // Ensure base resolvers is the correct data type
  if (Array.isArray(resolvers.base)) baseResolvers = resolvers.base
  else baseResolvers = [resolvers.base]
  // Ensure style resolvers is the correct data type
  if (Array.isArray(resolvers.styles)) styleResolvers = resolvers.styles
  else styleResolvers = [resolvers.styles]

  return {
    ...component,
    ...callResolvers(component, baseResolvers),
    style: {
      ...component.style,
      ...callResolvers(component, styleResolvers)),
    },
  }
}

const component = {
  type: 'div',
  style: {
    height: 250,
    fontSize: 14,
    fontWeight: 'bold',
    textAlign: 'center',
  },
  children: [
    {
      type: 'input',
      inputType: 'email',
      placeholder: 'Enter your email',
      style: {
        border: '1px solid magenta',
      },
    },
  ],
}

const result = start(component, {
  resolvers: {
    base: [resolveTimestampInjection, resolveChildren],
    styles: [resolveStyles],
  },
})
Enter fullscreen mode Exit fullscreen mode

This is where the power of composing higher order functions begin to shine, and the good news is that they're easy to implement!

Abstracting away the abstractions

To enable this functionality, lets move one step higher in the abstraction by wrapping the resolvers into a higher order function that is responsible for injecting the context to the lower level resolver functions.

function makeInjectContext(context) {
  return function (callback) {
    return function (...args) {
      let result = callback(...args)
      if (typeof result === 'function') {
        // Call it again and inject additional options
        result = result(context)
      }
      return result
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

We can now return a function from any function that we register as a resolver and still maintain the behavior of our app the same, like so:

const getBaseStyles = () => ({ baseStyles: { color: '#333' } })
const baseStyles = getBaseStyles()

const injectContext = makeInjectContext({
  baseStyles,
})

function resolveTimestampInjection(component) {
  return function ({ displayTimestamp }) {
    if (displayTimestamp === true) {
      return {
        time: new Date(currentDate).toLocaleTimeString(),
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Before I show the final example, lets go over the makeInjectContext higher order function and go over what it is doing:

It first takes an object that you want to be passed to all the resolver functions and returns a function that takes a callback function as an argument. This callback parameter will later become one of the original resolver functions. The reason why we do this is because we're doing whats referred to as wrapping. We wrapped the callback with an outer function so that we can inject additional functionality while still maintaining the behavior of our original function by ensuring that we call the callback inside here. If the return type of the callback's result is a function, we will assume that callback needs the context so we call the callback's result one more time--and that is where we pass in the context.

When we call that callback (a function provided by the caller) and do some computation inside the wrapper function, we have values coming from the wrapper and from the caller. This is a good use case for our end goal because we wanted to merge results together instead of enabling each resolver function the ability to override a value or result from a previous resolver function! It's worth nothing that there are other advanced use cases to solve different problems, and this is a good example to showcase a situation where we needed the right strategy to use for the right situation--because if you're like me, you probably tried to implement a lot of advanced use cases every time you see an open opportunity--which is bad practice because some advanced patterns are better than others depending on the situation!

And now our start function needs to adjust for the makeInjectContext higher order function:

const getBaseStyles = () => ({ baseStyles: { color: '#333' } })

function start(component, { resolvers = {}, displayTimestamp }) {
  const baseStyles = getBaseStyles()
  // This is what will be injected in the returned function from the higher order function
  const context = { baseStyles, displayTimestamp }
  // This will replace each original resolver and maintain the behavior of the program to behave the same by calling the original resolver inside it
  const enhancedResolve = makeInjectContext(context)

  let baseResolvers
  let styleResolvers

  // Ensure base resolvers is the correct data type
  if (Array.isArray(resolvers.base)) baseResolvers = resolvers.base
  else baseResolvers = [resolvers.base]
  // Ensure style resolvers is the correct data type
  if (Array.isArray(resolvers.styles)) styleResolvers = resolvers.styles
  else styleResolvers = [resolvers.styles]

  return {
    ...component,
    ...callResolvers(component, baseResolvers.map(enhancedResolve)),
    style: {
      ...component.style,
      ...callResolvers(component, styleResolvers.map(enhancedResolve)),
    },
  }
}

const component = {
  type: 'div',
  style: {
    height: 250,
    fontSize: 14,
    fontWeight: 'bold',
    textAlign: 'center',
  },
  children: [
    {
      type: 'input',
      inputType: 'email',
      placeholder: 'Enter your email',
      style: {
        border: '1px solid magenta',
      },
    },
  ],
}

const result = start(component, {
  resolvers: {
    base: [resolveTimestampInjection, resolveChildren],
    styles: [resolveStyles],
  },
})
Enter fullscreen mode Exit fullscreen mode

And we still get an object back with the expected results!

{
  "type": "div",
  "style": {
    "height": 300,
    "fontSize": 14,
    "fontWeight": "bold",
    "textAlign": "center"
  },
  "children": [
    {
      "type": "input",
      "inputType": "email",
      "placeholder": "Enter your email",
      "style": {
        "border": "1px solid magenta"
      },
      "textTransform": "uppercase"
    }
  ],
  "time": "2:06:16 PM"
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

And that concludes the end of this post! I hope you found this to be valuable and look out for more in the future!

Find me on medium

πŸ’– πŸ’ͺ πŸ™… 🚩
jsmanifest
jsmanifest

Posted on June 10, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related