DEV Community

JavaScript Joel
JavaScript Joel

Posted on • Updated on

Functional JavaScript: Function Decorators Part 2 #JavaScript

This article was originally posted on Medium.

The Beginning

Function decorators allow you to enhance existing functions without modification to the original function.

In Part 1, I demonstrated how function decorators can transform a callback into a promise and back again. But function decorators are much more useful than the limited scope of callbacks and promises, so I thought this subject could use a reboot.

I think showing a bunch of examples will be the best way to showcase function decorators, so this article will be a little light on words and focus more on the code.

The Hello World of a function decorators

// basic decorator (supports 1 arity functions)
const decorator = f => a => f(a)
Enter fullscreen mode Exit fullscreen mode

To support n-arity functions we can expand it to this (still does nothing).

// basic decorator (supports n-arity functions)
const decorator = f => (...args) => f(...args)
Enter fullscreen mode Exit fullscreen mode

Now let’s create and use a helloWorld decorator to decorate the add function.

// our Hello World decorator
const helloWorld = f => (...args) => {
  console.log('Hello World')
  return f(...args)
}

// function to be decorate
const add = (a, b) => a + b

// decorate the function
const helloWorldAdd = helloWorld(add)

// run it
helloWorldAdd(3, 4)
//=> "Hello World"
//=> 7
Enter fullscreen mode Exit fullscreen mode

Use this base decorator as a template for any function decorator you want to create.

Logging Function Decorator

Easily wrap your logging logic around existing functions.

// note: logged function name may change with minification
const logger = f => (...args) => {
  const value = f(...args)
  console.log(`${f.name||'Anonymous'}(${args}) = ${value}`)
  return value
}

const add = (a, b) => a + b

// we can make a new decorated function like so
const logAdd = logger(add)
logAdd(3, 4)
//=> "add(3, 4) = 7"
//=> 7
Enter fullscreen mode Exit fullscreen mode

Homework: How would you modify this to also support Asynchronous functions? For a hint, look below inside the timed function decorator.

Timer Function Decorator

Basic timer function that works with both synchronous and asynchronous code.

Line 15 checks if the value is a promise and puts the return value into a then instead of returning it.

// timed decorator
const timed = f => (...args) => {
  const start = Date.now()
  const value = f(...args)
  return value && typeof value.then === 'function'
    ? value.then(value =>
      ({ timespan: Date.now() - start, value }))
    : { timespan: Date.now() - start, value }
}

// synchronous function
const fastFunction = x => x * 2

// asynchronous function
const slowFunction = x => new Promise(resolve =>
  setTimeout(() => resolve(x * 2), 2000)
)

timed(fastFunction)(123)
//=> { timespan: 0, value: 246 }

timed(slowFunction)(456)
//=> Promise: { timespan: 2000, value: 912 }
Enter fullscreen mode Exit fullscreen mode

Function Parameter Guard Decorator

Guard all parameters against null or undefined.

// requireAll decorator
const requireAll = f => (...args) => {
  const valid = args.filter(arg => arg != null)
  if (valid.length < f.length) {
    throw new TypeError('Argument cannot be null or undefined.')
  }
  return f(...valid)
}

// decorated add function
const add = requireAll((a, b) => a + b)

add(3, 4)
//=> 7

add(3)
//=> TypeError: Argument cannot be null or undefined.

add(null, 4)
//=> TypeError: Argument cannot be null or undefined.
Enter fullscreen mode Exit fullscreen mode

Homework: How could this decorator be enhanced? How would you add argument names? How would you guard against only some arguments?

Exception Handling

Instead of throwing an exception, you can return an object that will contain either the value or an error. This is similar to how the Either monad handles it’s values. (don’t worry about monads right now).

// function decorator to catch exceptions
const tryCatch = f => (...args) => {
  try {
    return { error: null, value: f(...args) }
  } catch(err) {
    return { error: err, value: null }
  }
}

const double = x => {
  if (!isNumeric(x)) {
    throw new TypeError('value must be numeric')
  }

  return x * 2
}

// new "safe" double function
const safeDouble = tryCatch(double);

safeDouble('abc')
//=> { error: [TypeError: value must be numeric], value: null }

safeDouble(4)
//=> { error: null, value: 8 }
Enter fullscreen mode Exit fullscreen mode

Homework: Research and learn to use the Either Monad. Change this code to return an Either.

Fetch JSON Function Decorator

When using fetch it is common to see code like this sprinkled throughout your codebase:

fetch(url)
  .then(response => response.json())
  .then(data => /* now we can use data! */)
Enter fullscreen mode Exit fullscreen mode

To get at that json, you always have to call response.json() first.

// function decorator
const withJson = f => url =>
  f(url).then(response => response.json())

// decorated function
const fetchJson = withJson(fetch)

// now all your fetch calls are simplified
fetchJson(url)
  .then(data => /* we are ready to go! */)
Enter fullscreen mode Exit fullscreen mode

Currying

If you are familiar with currying functions like Ramda’s curry, then you may already be familiar with function decorators.

// basic curry function decorator
const curry = f => (...args) =>
  args.length >= f.length
    ? f(...args)
    : curry(f.bind(undefined, ...args))

const multiply = curry((a, b) => a * b)
const double = multiply(2)

double(3)
//=> 6

multiply(3, 3)
//=> 9

multiply(4)(4)
//=> 16
Enter fullscreen mode Exit fullscreen mode

Note: I recommend using a more mature curry function, like the one from Ramda. Even though this one will work just fine, it is provided for example only.

Next.js browser check

In a Next.js project I was creating, I had to limit a couple of functions to only executing on the browser side. I was able to do this cleanly with a simple function decorator.

const onlyBrowser = f => () =>
  process.browser && f()

class MyComponent extends Component {
  componentWillMount = onlyBrowser(() =>
    console.log('This is the browser!')
  })
}
Enter fullscreen mode Exit fullscreen mode

Multiple Ways to Decorate

There are a few ways to decorate functions. How you decide to use decorators will depend on your use-case.

// decorate the function
const add = decorator((a, b) => a + b)

// create a new decorated function
const add = (a, b) => a + b
const decoratedAdd = decorator(add)

// decorate just for the call
const add = (a, b) => a + b
decorator(add)(3, 4)
Enter fullscreen mode Exit fullscreen mode

Combining Function Decorators

Because each decorator also returns a function, function decorators can easily be combined to create one mega function.

// returns true if is numeric
const isNumeric = n => !isNaN(parseFloat(n)) && isFinite(n)

// function decorator for only numeric args
const onlyNumeric = f => (...args) => {
  const valid = args.filter(isNumeric)
  if (valid.length < f.length) {
    throw new TypeError('Argument must be numeric.')
  }
  return f(...valid)
}

// function decorator to catch exceptions
const tryCatch = f => (...args) => {
  try {
    return { error: null, value: f(...args) }
  } catch(err) {
    return { error: err, value: null }
  }
}

// our double function
const double = x => x * 2

// decorated double function
const safeDouble = tryCatch(onlyNumeric(double));

safeDouble('abc')
//=> { error: [TypeError: value must be numeric], value: null }

safeDouble(4)
//=> { error: null, value: 8 }
Enter fullscreen mode Exit fullscreen mode

You can also use function composition to combine decorators

// function composer
const compose = (f, g) => x => f(g(x))

// returns true if is numeric
const isNumeric = n => !isNaN(parseFloat(n)) && isFinite(n)

// function decorator for only numeric args
const onlyNumeric = f => (...args) => {
  const valid = args.filter(isNumeric)
  if (valid.length < f.length) {
    throw new TypeError('Argument must be numeric.')
  }
  return f(...valid)
}

// function decorator to catch exceptions
const tryCatch = f => (...args) => {
  try {
    return { error: null, value: f(...args) }
  } catch(err) {
    return { error: err, value: null }
  }
}

// compose two decorators into one decorator
const tryCatchOnlyNumeric = compose(tryCatch, onlyNumeric)

// decorated double function
const double = tryCatchOnlyNumeric(x => x * 2)

double('abc')
//=> { error: [TypeError: value must be numeric], value: null }

double(4)
//=> { error: null, value: 8 }
Enter fullscreen mode Exit fullscreen mode

React

React and the entire ecosystem is filled with function decorators. If you have used React, there’s a high likely-hood you have already used function decorators. react-redux's connect is a function decorator. redux’s bindActionCreators is a function decorator.

The End

Function decorators are powerful tools used to enhance existing functions. They are not something new and if you haven’t already used a function decorator, it is very likely you will use them in your near future.

Even though they are so powerful and easy to create, I don’t see many people creating function decorators in their code. This tells me function decorators are under utilized tool that are worthy of more exploration.

Don’t forget to do the homework in this article!

I would love to hear how would you use function decorators today to improve your codebase in the comments below! 😃

Cheers!

Follow me

Twitter: https://twitter.com/joelnet
LinkedIn: https://www.linkedin.com/in/joel-thoms/
Medium: https://medium.com/@joelthoms/latest
Dev.to: https://dev.to/joelnet
Github: https://github.com/joelnet

Top comments (0)