In the following code I am trying to add decorators to a function. For certain reasons I would like to display the function attribute "name". However, I have no access to it as soon as I am in the individual functions. Also, I'm not sure why the functions are called from the bottom up. What are the reasons for all of the points mentioned and how can I avoid them?
let rectangleArea = (length, width) => {
return length * width;
}
const countParams = (fn) => {
return (...params) => {
console.log('countParams', fn.name)
if (params.length !== fn.length) {
throw new Error(`Incorrect number of parameters for ${fn.name}!`);
}
return fn(...params);
}
}
const requireIntegers = (fn) => {
return (...params) => {
console.log('requireIntegers', fn.name)
params.forEach(param => {
if (!Number.isInteger(param)) {
throw new TypeError(`Params must be integers at ${fn.name}!`); //Can't access fn.name
}
});
return fn(...params);
}
}
//Why running from bottom to top?
rectangleArea = countParams(rectangleArea);
rectangleArea = requireIntegers(rectangleArea);
console.log(rectangleArea(20, 30, "hey"));
The first time you make a decorated function for a given function, that returned function does not have a name -- it is anonymous. So when you then pass that decorated function to be decorated again, fn will be that anonymous function.
To solve this, assign the name of the fn function also to the returned decorated function. That way the name will stick even when you decorate that function again, and again...
Here is a helper function that will assign the name property to a given function:
const setName = (deco, value) => {
Object.defineProperty(deco, "name", {value, writable: false});
return deco;
}
let rectangleArea = (length, width) => {
return length * width;
}
const countParams = (fn) => {
return setName((...params) => {
console.log('countParams', fn.name)
if (params.length !== fn.length) {
throw new Error(`Incorrect number of parameters for ${fn.name}!`);
}
return fn(...params);
}, fn.name);
}
const requireIntegers = (fn) => {
return setName((...params) => {
console.log('requireIntegers', fn.name)
params.forEach(param => {
if (!Number.isInteger(param)) {
throw new TypeError(`Params must be integers at ${fn.name}!`); //Can't access fn.name
}
});
return fn(...params);
}, fn.name);
}
rectangleArea = countParams(rectangleArea);
rectangleArea = requireIntegers(rectangleArea);
console.log(rectangleArea(20, 30, "hey"));
Why the functions are called from the bottom up.
Because in your decorator the last step is to call fn.
That fn might be an already decorated function, and so it is normal that earlier decorations of the function run later.
It is like wrapping a birthday present several times, each time with a different color of wrapping paper. When your friend unpacks it, they will get to see the colors of wrapping paper in the reverse order from the order in which you had applied them.
So you want to do something additional with your decorators? They need some common behavior? We've shown we know how to do that already: with decorators. Your decorators need decorators of their own!
Here I write a decorator-decorator keep which takes a decorator function and returns a new decorator function which keeps the name and length properties of the function passed to it. (Say that five times fast!)
It uses the same technique as the answer from trincot, but is less intrusive, as you can simply wrap the decorator functions just as you would the underlying ones. Here I do that at definition time, since we never really want these decorators without this behavior, but you can do it as you like.
let rectangleArea = (length, width) => {
return length * width;
}
const keep = (decorator) => (fn) =>
Object .defineProperties (decorator (fn), {
name: {value: fn .name, writable: false},
length: {value: fn .length, writable: false}
})
const countParams = keep ((fn) => {
return (...params) => {
console.log('countParams', fn.name)
if (params.length !== fn.length) {
throw new Error(`Incorrect number of parameters for ${fn.name}!`);
}
return fn(...params);
}
})
const requireIntegers = keep ((fn) => {
return (...params) => {
console.log('requireIntegers', fn.name)
params.forEach(param => {
if (!Number.isInteger(param)) {
throw new TypeError(`Params must be integers at ${fn.name}!`); //Can't access fn.name
}
});
return fn(...params);
}
})
//Why running from bottom to top? -- answered by @balastrong
rectangleArea = countParams(rectangleArea);
rectangleArea = requireIntegers(rectangleArea);
console.log(rectangleArea(20, 30));
console.log(rectangleArea(20, 30, "hey"));
.as-console-wrapper {max-height: 100% !important; top: 0}
The name keep was originally keepName before I realized that I personally would also want the function to keep the arity intact. I couldn't come up with a clear useful name for this... which is a large warning sign to me. So there may be something still wrong with this design.
It's not from bottom to top, it's the order you set.
With your decorators, you basically just did this:
requireIntegers(countParams(rectangleArea(20, 30, "hey"))).
Which means first it executes requireIntegers by passing it as input everything else (countParams(rectangleArea(20, 30, "hey"))).
Then you see the console log and the error as params.forEach scans the params and finds 'hey' which is not a number.