-
Notifications
You must be signed in to change notification settings - Fork 6
Description
One common pattern in JavaScript is to pass a closure as configuration to a function, as in hooks-like functional APIs:
function useApiResponse() {
return useQuery({
async queryFn() {
const response = await fetch('www.example.com/api');
return response.json();
}
});
}
Currently, there is no way to easily memoize such an API because each time this code executes, queryFn
creates a new function. While we could potentially use toString()
for this, performance is questionable (arbitrarily long, and unknown if engines optimize for that path), and it may not work with function implementation hiding if that proposal ever gets merged. It also does not work when the closure function actually closes over some in-scope variable:
function useApiResponse(url) {
return useQuery({
async queryFn() {
const response = await fetch(url);
return response.json();
}
});
}
Let's assume that the API response always return the same value for a given URL for now. If that is the case, then in theory, the result of useQuery
should be the same based on the URL it receives:
useApiResponse('www.example.come/api');
useApiResponse('www.example.come/api'); // same result as first time
useApiResponse('www.other.come/api'); // different result
Given this, we conceptually should be able to memoize useQuery
based on:
- The function definition (e.g. the string of code that each closure instantiates based on)
- The scopes that the closure captures (same scopes === same closed over vars === same result when called with same args)
For libraries that are exploring more intuitive and performant functional APIs, it would be really useful to be able to have a key based on these two properties. Currently, in my library Signalium, I'm needing to use a Babel transform that wraps callbacks:
const useApiResponse = reactive((url) => {
return useQuery({
queryFn: callback(
async () => {
const response = await fetch(url);
return response.json();
},
0, // address of closure in the function
[url], // in scope variables
}
});
});
This API is notably similar to useCallback
from React, with the only difference being the second parameter, the closure "address". This is basically a way to find the same closure in a given context, with the alternative being we just have an implicit counter in scope like React does, but then we have to always call things in the same order.
I mention Signalium here only as a non-React example use-case, because I want to show that this functionality would be useful outside of one programming model. My feeling, based on my experience building Signalium, is that it would be useful in many cases where we want to memoize values that are somehow tied to arguments that are called frequently, and that closures are often used as arguments.
So, with all that context, my questions:
- Has function equality been in general considered, outside of the obvious
===
equality that already is possible (but limited)? - Could this type of functionality be in scope for Composites? Would it make more sense as a separate proposal (e.g. something like
Function.getUniqueKey(closure)
?) - Is this something that would make sense in the language at all? I definitely wouldn't want to expose the actual scopes of closures, that would be far too powerful, but being able to basically do a quick deep-equal to get a key seems to me like it would be OK. I can't think of any way this could be abused currently, but I would love to see if anyone else sees an obvious flaw or issue it could introduce.