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,
});
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,
});
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);
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(),
}));
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',
}));
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',
});
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.
- If the input is a function, return the input unchanged.
- 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;
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)();
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);
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)()
}));
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)()
}));
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),
};
};
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);
};
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)