I’m trying to pass a bound method to a function, but I can’t quite figure out the syntax. It seems like javascript wants to somehow differentiate between an unbound method and a bound method, but where the calling context changes that. (Most programming languages simply define a method as a bound class function).
What’s the syntax for passing a bound method to a function, and then calling that bound method in the new scope?
Here’s the relevant snippets
const mongoose = require('mongoose');
const caching = require('../lib/caching')
const Blog = mongoose.model('Blog');
// This is where I'm trying to pass a method to another function. I'm unclear of the syntax here
Blog.find = caching.makeCachable(Blog.find.bind(Blog), Blog)
module.exports = app => {
app.get('/api/blogs', requireLogin, async (req, res) => {
let blogs = await Blog.find({_user: req.user.id});
return res.send(blogs);
});
// ... other routes
}
caching.js
const util = require('util');
const redis = require('redis');
const client = redis.createClient('redis://localhost:6379');
const asyncGet = util.promisify(client.get).bind(client);
const DEFAULT_CACHING = [
'EX', 60 * 60 * 1,//caching expires after 4 hours
]
// This is where I take the method and pass it on to another function. I hope this just passes through
function makeCachable(method, thisObject) {
console.log(method, thisObject, `${method.className}.${method.name}`);
return cachedQuery.bind(method, `${thisObject.className}.${method.name}`);
}
async function cachedQuery(queryFunction, queryKey, queryParams=null, cacheConfig=DEFAULT_CACHING) {
//check redis before executing queryFunction
const redisKey = JSON.stringify([queryKey, queryParams]);
const cacheValue = await asyncGet(redisKey);
if(cacheValue) {
return JSON.parse(cacheValue);
}
// This is where I try to call the bound method
const blogs = await queryFunction.call(queryParams);
if(blogs) {
client.set(redisKey, JSON.stringify(blogs), ...cacheConfig);
}
return blogs;
}
exports.makeCachable = makeCachable;
EDIT
Apparently I wasn’t clear enough about what doesn’t work. This version of my code is after trying several combinations that don’t work, but the error is always similar. For example, if I try to invoke const blogs = await queryFunction(queryParams);
I get the following error
[0] E:DocumentsCodenodejs_classAdvancedNodeStarterlibcaching.js:23
[0] const blogs = await queryFunction(queryParams);
[0] ^
[0]
[0] TypeError: queryFunction is not a function
[0] at Function.cachedQuery (E:DocumentsCodenodejs_classAdvancedNodeStarterlibcaching.js:23:25)
[0] at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
[0] at async E:DocumentsCodenodejs_classAdvancedNodeStarterroutesblogRoutes.js:22:17
[0]
Similarly, I’ve tried, Blog.find = caching.makeCachable(Blog.find);
, Blog.find = caching.makeCachable(Blog.find.bind(Blog));
, I’ve tried invoking with .call()
and without .call()
. In all cases, I get some kind of error like the one above.
What I would expect from all other programming languages, with some slight syntactic differences is that Blog.find is a bound method. For example, C++ would let you reference &Blog.find
or &Model::find
for bound/unbound methods, respectively. Python lets you simply refer to Blog.find
or Model.find
for bound/unbound methods, respectively.
It seems in some situations javascript expects you to explicitly bind it. But for whatever reason, when I pass the function, bound or not, to makeCachable it seems to stop being a function. What is the actual syntax for passing a bound method to another function? Is there something extra that needs to be done to bind a method to another function parameter?
8
.bind
accepts one or more arguments:
- The first will be used as the
this
value within the function - The remaining arguments are curried, being used in order as the parameters of the function
Where you are doing:
cachedQuery.bind(method, `${thisObject.className}.${method.name}`)
What that actually means is that, within cachedQuery
, the this
value (that you’re not using and don’t need) is bound to method
, and the queryFunction
parameter is set to ${thisObject.className}.${method.name}
.
If you’re trying to just curry without binding anything, you could do:
cachedQuery.bind(null, method, `${thisObject.className}.${method.name}`)
However, note that there is no .className
property on functions or objects, so this would always be undefined
and your queryKey
would look like 'undefined.find'
. I assume you’re looking for 'Blog'
. The closest equivalent would be thisObject.constructor.name
, but I would strongly recommend against using that as a pattern. Unlike class-based languages, the value doesn’t have to be an instance of a class at all, e.g. const Blog = { find: () => null }
, so it would just be 'Object'
.
Thus, I think it would be clearer to supply the query key and do the currying explicitly:
Blog.find = caching.makeCacheable('Blog.find', Blog.find.bind(Blog))
// small nitpick, the word is cacheable, not cachable
function makeCacheable(queryKey, queryFunction) {
return (queryParams = null, cacheConfig = DEFAULT_CACHING) => {
return cachedQuery(queryFunction, queryKey, queryParams, cacheConfig)
}
}
// no longer needs the default values as they're done in the curried function
// otherwise the same
async function cachedQuery(queryFunction, queryKey, queryParams, cacheConfig) {
// ...
}
1
Ok, I still don’t know why, but here’s a workaround. If I just change the makeCacheable function to use a closure instead of invoking bind explicitly, it works
function makeCachable(method, thisObject) {
queryKey = `${thisObject.className}.${method.name}`;
return function(queryParams=null, cacheConfig=DEFAULT_CACHING) {
return cachedQuery(method, queryKey, queryParams, cacheConfig);
}
}
I don’t have an explanation for why return cachedQuery.bind(method, queryKey);
doesn’t work, but the closure does work.