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.