1

Consider the following Typescript code:

function exampleAction(target: Target, options: ExampleActionOptions) {
  // ... 
}

export function getExampleAction(options: ExampleActionOptions) {
  return (target: Target) => exampleAction(target, options);
}

Here we have two functions. One that has the actual implementation, and another that takes one of the original functions parameters and returns a new function that takes the remaining parameter and calls the original with both parameters. I have been informed that this is called partial application.

Then elsewhere, we create the function that now has a constant state and call it:

import { getExampleAction } from "./example-actions";
import { exampleTarget } from "./targets"

const customExampleAction = getExampleAction({
  // ...
});

const result = customExampleAction(exampleTarget);

Are there any drawbacks to this sort of thing? It seems like we are attaching a state to a function, would there be any issues debuging this code?

2
  • 4
    Not only it's allowed, but there's often language support for it (look up partial application and currying). Also, you kind of (re)discovered OOP here (at least, classes & objects aspect of it): getExampleAction is like a constructor that takes one argument, and the returned function is like an (immutable) object with a single method and an encapsulated field. Commented Feb 5, 2023 at 16:10
  • Yeah, I had a bit of code that had a global state, I made it stateless and then added a class. Seing how simple the class was I thought this might be a good way to keep it in the functional paradigm. Thanks for those terms. I guess I should reword my question. Commented Feb 5, 2023 at 17:28

1 Answer 1

6

This is an entirely normal and reasonable functional programming pattern.

But some people are not used to such patterns, and some abstractions are not worth it.

For example, because you are providing a custom function for retrieving a partially-applied action, it is not immediately obvious that exampleAction and getExampleAction would be related. It could be better to only offer one of them, e.g.

const exampleAction = (target, options) => ...;

or

const exampleAction = (options) => (target) => ...;

Similarly, a reader of your customExampleAction does not immediately see that this too is a function, because you have essentially used point-free style here. While that style is considered to be idiomatic in Haskell, it is rarely known in the JavaScript community. It could be better to visibly use a function here, for example:

const customExampleAction = (target) => exampleAction(target, {...});

It is entirely possible that in your code base, you are partially-applying example-actions so often that a getExampleAction would be worth it. But every function, every abstraction introduces additional complexity into the system. Abstractions must earn their keep – on balance, they must make the system more simple. Sometimes they achieve this through deduplicating common code (“three or more? use a for!”), sometimes they achieve this by encapsulating some complexity. But a function for partially-applying one specific function encapsulates and deduplicates very little.

You are also concerned about using partial application in general, since this introduces hidden state.

On one hand, this isn't an issue any more than creating objects. Objects and closures are equivalent on a very fundamental level – they can be implemented in terms of each other, and programming languages usually implement them using very similar techniques.

But there are some practical differences. An advantage of closures and other functional patterns is that they emerge implicitly from the code, whereas objects and data structures have to be created explicitly (though JS makes this much easier than in other languages). But what is simpler while writing the code turns into a drawback when trying to debug the system: debuggers tend to have better facilities for inspecting members of an object than for inspecting captured variables of a closure.

For example, consider the following examples for adding two numbers in a partially-applied manner:

// return a function that closes over "a"
const f = a => b => a + b;

// return an object with an "a" member
const o = a => ({
  a,
  add(b) {
    return this.a + b;
  }
});

// invoke the two functions and log them
let f40 = f(40);
let o40 = o(40);
console.log(f40, o40);

Run that code e.g. in a browser developer console and try to figure out what the value of a was for the two objects. For o40 this is trivial because the browser development tools directly show us the object member a: 40. For the function, we have no direct access, though we should be able to see that variable if we step through the function in a debugger.

tested in Firefox 109 and Chrome 109

This is not an inherent problem – this is just the tooling as we have it right now. But right now, highly functional tooling that depends a lot on closures tends to be more difficult to inspect and debug than more conventional procedural or object-oriented code. Quite often, this doesn't matter. It's just something to be on the lookout for, and it could tip the scales towards a more explicit, boring solution.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.