Symbol.callable for callable object

Erm, I would be veery surprised if this hasn't already been proposed (or maybe it's that dumb of an idea :upside_down_face:), but I couldn't find any discussion on this.

I'd like to have a Symbol.callable to make an object "callable", similar to how Symbol.iterator makes it iterable. For example:

class Foo {
    someProp = 'foo';
    // some other fields and methods...
    constructor(x = 0) {
        this[Symbol.callable] = (y) => x + y;
    } 
}

const f = new Foo(1);
f(2); // -> 3

Currently, the only way I've seen to achieve something like this is to assign properties to a function, like so:

const newFoo = (x = 0) => {
    const func = (y) => x + y;
    func.someProp = 'foo';
    return func;
}
const f = newFoo(1);

I think this is kind of ugly/weird and it's awkward if you want to make it behave like an actual instance of an object (with its prototype pointing to Foo). In the latter case, you'd need to do that:

function Foo (x = 0) {
    const func = (y) => x + y;
    func.someProp = 'foo';
    return Object.setPrototypeOf(func, Foo.prototype);
}
const f = new Foo(1);

which is even weirder to me and the object doesn't get proper annotation in the console (it's displayed as a function, rather than a Foo object with someMethod).

Please let me know if this has been discussed and what was the discussion.

1 Like

It’s been proposed for classes, but i doubt it would ever work as a generic protocol because it would likely slow down calling behavior for all functions.

More important than the suggested solution, though, is the problem - what problem are you solving?

Thanks for replying @ljharb . Do you know what the consensus was on the proposal for classes, do you have a link?

There's nothing more to the problem, than what I stated. I would like to create a custom object that can be invoked like a function but also have other properties/methods and a custom prototype (that can be checked with instanceof). The example I should using a function, assigning properties and a prototype to it, does what I want, but I find it very unappealing and against the class design.

Why would you like to create that? Wouldn’t that be surprising to users when nothing else in the language has that interface?

I guess what I'm suggesting is that the language implements that interface.

As long as it's documented, I don't see why it would be surprising to users.

A custom callable can be passed to external methods or used by users that expect a function but that same object can be passed to methods that are aware of this type and can handle it in a different way. I hope this makes sense.

A real world example would be the Mocha test framework. They provide an it() and describe() function to define tests, but you can also do things such as it.only() to make it be the only test that gets ran. I've done similar stuff in one of my own libraries, where the primary export was a template tag, and I chose to attach various helpers/tools as properties to that template tag.

I can't currently think of any real world examples where having a custom prototype to enable a working instanceof check is needed.

1 Like

Related thread: Function object initializer

2 Likes

Callable is an intrinsic trait of objects, and I don't believe it should become a protocol. Since you can already fairly easily create callable objects and add properties, I don't think it's really needed either. For instanceof checks there's already a protocol for that to avoid having to update the proto of the callable. Outside of that and replicating class inheritance, the use cases for a different proto in callables are so unclear that relying on setPrototypeOf feels sufficient for them.

Edit: that said I could be convinced by a declarative way of declaring custom callable objects like object literals.

Sorry, I didn't get that, how does one implement instanceof check without updating the proto of the callable?

Use Symbol.hasInstance on your "constructor"

1 Like

I don't love Symbol.hasInstance. it just feels like a lie - claiming that an object is an instance of a value without there actually being a prototype in common. I would prefer mangling prototypes over using it.

Anyways, I am realizing a problem with a callable protocol - if you can make any object callable, it means functions such as .call() won't be directly available, which means you can't blindly pass callable objects into places that expect functions as parameters.

Perhaps some declarative approach, like you mentioned, where perhaps you can declare a function with custom properties using syntax would be better, but it feels like a hard sell - these kinds of things do appear in the wild, but they're not extremely common.

And, we sort of already have a declarative approach - we can do { __proto__() { ... }, otherFn() { ... } }.

__proto__() {} does not declare the prototype. The only way to declare a prototype is with __proto__: ..., so you probably want __proto__: function () {} instead.

Also, a callable prototype does not make the object itself callable; you need to actually declare it as a function to make it callable.

1 Like

I would have thought that it's possible to implement this such that if we defined Symbol.callable on an object (and the value would obviously be a function), it would transparently proxy .call, .bind and .apply to that function as well. Is it not? I understand it may create a clash in case the object also defines those properties, but that's also the case when you use a plain function with custom properties (you can always override methods on Function.prototype.

Huh, you're right. That first part seems really weird, I wonder what the rational is for having __proto__() { ... } syntax behave differently from __proto__: function() { ... }.

Things like this add an overhead to the whole language. Now every property lookup needs to check if the prop is "call" and then also check for Symbol_callable. This overhead applies both to the implementation and also when trying to reason about the language

3 Likes

Edit: short-hand function object declaration discussion moved to Function object initializer - #15 by mhofman

Wait, what is (obviously) a function under this proposal? Does that no longer mean the same as "callable object"? Otherwise we'd have to recursively check if obj[Symbol.callable] is an object with a [Symbol.callable] property and so on...

Good catch on the language use. Yes, I meant that the value of Symbol.callable would itself be a callable. Under this proposal then whenever a non-native function object is invoked, then the Symbol.callable property would be checked and if not callable, then error is thrown. So yes, in theory it would naturally be recursive, but I don't think it will be an issue. It would be essentially the same as having a function invoke another function. It's possible to lead to infinite recursion but that's as a result of a wrong usage rather than a flow in the spec I think.

This seems problematic for engines that have a lot of optimizations built around function calls. This effect can be achieved by constructing the object on an object of type function, such as

const obj = () => {
    // This is the [callable] overload
}
obj.propA = 1
obj.propB = 2

or better yet

const obj = Object.assign(() => {
    // This is the [callable] overload
}, {
    propA: 1,
    propB: 2
}

This way our javascript stays reasonably fast ;)