DEV Community

Samuel Rouse
Samuel Rouse

Posted on

asFunction: JavaScript Functional Programming

Let's take a look at the choice between passing static data objects and functions that build them, and ask if we need to choose at all.

Passing Data

Passing data into a function is one of the earliest lessons in most programming languages and we usually start with simple functions that take simple values when building a project.

const getData = (url, filterData) => fetch(url, {
  body: filterData,
});
Enter fullscreen mode Exit fullscreen mode

Building Features

As we add complexity we may need information from multiple sources to make requests or display data.

const getData = (
  sessionHeaders,
  url,
  filterData,
) => fetch(url, {
  body: filterData,
  headers: sessionHeaders,
});
Enter fullscreen mode Exit fullscreen mode

Currying Common Code

In our example sessionHeaders are the same for all requests within a module. We can curry or partially-apply that argument so the code consuming the function avoids can still run without knowledge of those implementation details.

const setupGetData = curry((
  sessionHeaders,
  url,
  filterData,
) => fetch(url, {
  body: filterData,
  headers: sessionHeaders,
}));

// In another module, add the common headers.
const getData = setupGetData(userHeaders);
Enter fullscreen mode Exit fullscreen mode

The getData function signature for our module is unchanged but we now include the necessary header data.

Change Is The Only Constant

After a while we discover some module headers contain tokens that expire and must be refreshed. That creates a problem for our "static" data object.

We could mutate sessionHeaders from the source and allow the pass-by-reference nature of objects to solve the updates, but mutating objects requires assumptions about how the data is consumed and assumptions create the risk of defects. For example, memoization could cause incorrect results because we pass the same object even though the contents changed.

First-Class Functions Fix Failures

To ensure we get the latest sessionHeaders, we can change from passing an object to a function that returns an object.

const setupGetData = curry((
  getSessionHeaders,
  url,
  filterData,
) => fetch(url, {
  body: filterData,
  headers: getSessionHeaders(),
}));
Enter fullscreen mode Exit fullscreen mode

Now we get the latest data each time, but every place we use setupGetData must now pass a function, even if some of them don't require dynamic updates.

// In this module the headers don't change but we
//  need to add the function wrapper anyway.
const getNoCacheData = setupGetData(() => ({
  'Cache-Control': 'no-cache',
}));
Enter fullscreen mode Exit fullscreen mode

I know in reality Cache-Control is a response header, not a request header, but this example is made up anyway. 🤔

Fight Complexity With Flexibility

What if we could accept either data or a function? Then we support both cases and don't add useless functions where they aren't needed.

const setupGetData = curry((
  sessionHeaders,
  url,
  filterData,
) => fetch(url, {
  body: filterData,
  headers: typeof sessionHeaders === 'function'
    ? sessionHeaders()
    : sessionHeaders,
}));


// In this modules the headers change
const getData = setupGetData(getUserHeaders);

// In this module the headers are static
const getNoCacheData = setupGetData({
  'Cache-Control': 'no-cache',
});
Enter fullscreen mode Exit fullscreen mode

This is a pattern we can apply many places. To avoid adding the ternary for each argument we can make a utility for this.

asFunction

Requirements

The needs of this function are pretty simple.

  1. If the input is a function, return the input unchanged.
  2. If the input is not a function return a nullary function – one that takes no arguments – that returns the input.

That's it. No special handling for any other data types. Function or Not Function.

The Code

const asFunction = (input) => typeof input === 'function'
  ? input
  : () => input;
Enter fullscreen mode Exit fullscreen mode

It isn't much to look at, but it enables a lot of flexibility when it is used. So let's use it!

const output = asFunction(myObjectOrFunction)();
Enter fullscreen mode Exit fullscreen mode

When supporting an object or nullary function, you just wrap your value in asFunction and can then call it immediately. You can also pass values that will be ignored if asFunction is passed an object.

const output = asFunction(myFunctionOrObject)(options);
Enter fullscreen mode Exit fullscreen mode

asFunction in Action

Our implementation has no options or arguments to pass, so adding asFunction is simple.

const setupGetData = curry((
  sessionHeaders,
  url,
  filterData,
) => fetch(url, {
  body: filterData,
  headers: asFunction(sessionHeaders)()
}));
Enter fullscreen mode Exit fullscreen mode

Now we can accept our new functions without changing our previous static values. And we don't have to worry about switching back and forth as requirements change. Both styles are acceptable.

In fact, we could use this on all of our arguments if we want flexibility for the future.

const setupGetData = curry((
  sessionHeaders,
  url,
  filterData,
) => fetch(asFunction(url)(), {
  body: asFunction(filterData)(),
  headers: asFunction(sessionHeaders)()
}));
Enter fullscreen mode Exit fullscreen mode

This might be overusing the function, but it demonstrates how easily we can add support for dynamic content. Not every use supports this flexibility, and there are times when you may intentionally avoid it for consistency, but it is a very simple addition when you need it.

Making An Argument

asFunction can also be used in the opposite case...sometimes we want a static value where we normally expect a function.

const getOptions = (baseConfig, sectionOptions) => {
  // pass baseConfig to sectionOptions, just in case.
  return {
    ...baseConfig.options,
    ...asFunction(sectionOptions)(baseConfig),
  };
};
Enter fullscreen mode Exit fullscreen mode

Our function here assumes sectionOptions needs the baseConfig to build an object. When a plain object is passed, it ignores the arguments and returns the object for us.

I also talk about this in the post doIf (if/then/else): JavaScript Functional Programming.

const doIf = (predicate, consequent, alternative) => {
  return (...args) => asFunction(predicate)(...args)
    ? asFunction(consequent)(...args)
    : asFunction(alternative)(...args);
};
Enter fullscreen mode Exit fullscreen mode

Conclusion

Some small utilities can provide a lot of capability. asFunction allows you to create or update a function to support any single value as an object of function with minimal effort. It can be a helpful tool in making code flexible and breaking a problem down into smaller pieces to so it is easier to read, reason about, and maintain.

When you are building your next function, consider whether your arguments could be dynamic in this way and whether asFunction could make your code more capable.

Top comments (0)