I have a bunch of regular utility functions. I want to convert those functions to ones that accept arguments wrapped in functions (to introduce a side-effect when a value is used).
// Some utility function:
const pick = (takeLeft, left, right) =>
takeLeft ? left : right;
// Some wrapper with a side-effect
const Watched = x => () => {
console.log(`Value ${x} is used`);
return x;
};
// I want this to log:
// Value true is used
// Value L is used
const myPick = runWithWatchedValues(
pick,
Watched(true), Watched("L"), Watched("R")
);
I’m looking for help implementing runWithWatchedValues
, or for somebody to explain me why it can’t be done.
Attempts
The problem with unwrapping the values before calling the inner function:
// An incorrect attempt: (also logs "R")
const runWithWatchableValues = (f, ...args) =>
f(...args.map(w => w()));
I tested if calling the function with apply
and some special getters would work, but it turns out this does exactly the same .
// Another incorrect attempt: (also logs "R")
const runWithWatchableValues = (f, ...watched) => {
const args = watched.reduce(
(acc, get, i) => Object.defineProperty(acc, i, { get }),
[]
);
return f.apply(null, args);
};
My current solution is to manually rewrite utility functions. I.e.:
const pick = (takeLeft, left, right) =>
takeLeft ? left : right;
const pickW = (takeLeft, left, right) =>
takeLeft() ? left() : right();
// Correctly logs true and L
pickW(Watched(true), Watched("L"), Watched("R"));
I’d rather not maintain my own library of utility functions when there are well documented and well maintained libraries like ramda or lodash…
The question(s)
I’m starting to feel like the thing I want just is just not something the language can do… But I hope I’m wrong!
- Is it theoretically possible to write
runWithWatchableValues
and get the desired result?- If yes, how?
- If no, is there another automated way (Babel/build steps?) you can think of to prevent having to manually rewrite
pick
to work with wrapped values?
“This is an x/y problem!”
It might be, but I wanted to keep it simple. Here’s what I’m really doing (using knockout.js):
const pick = (takeLeft, left, right) =>
takeLeft ? left : right;
const takeLeft = ko.observable(true);
const left = ko.observable("L");
const right = ko.observable("R");
// BROKEN: The easy, but wrong implementation:
const myPick = ko.pureComputed(
() => pick(takeLeft(), left(), right())
);
console.log(
"Naive approach:",
myPick(), // Right: "L"
myPick.getDependenciesCount() // Wrong: 3
);
// The dependency on `right` will mean that updating
// it will cause myPick to re-evaluate, even though we
// already know its return value won't change.
// FIXED: The manual fix:
const pickObs = (takeLeft, left, right) =>
takeLeft() ? left() : right();
const myCorrectPick = ko.pureComputed(
() => pickObs(takeLeft, left, right)
);
console.log(
"With manual rewrite:",
myCorrectPick(), // Right: "L"
myCorrectPick.getDependenciesCount() // Right: 2
);
// Changing `right` doesn't do anything. Only once `takeLeft`
// is set to `false`, a dependency on `right` will be created
// (and the dependency on `left` will be removed).
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>