asFunction: JavaScript Functional Programming
Tags: webdev, javascript, programming
Published: 2023-12-21 13:53:40

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.

```javascript
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.

```javascript
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.

```javascript
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.

```javascript
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.

```javascript
// 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.

```javascript
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.

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

```javascript
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!

```javascript
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.

```javascript
const output = asFunction(myFunctionOrObject)(options);
```

### `asFunction` in Action

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

```javascript
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.

```javascript
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.

```javascript
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](https://dev.to/oculus42/doif-ifthenelse-javascript-functional-programming-4ni).

```javascript
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.