1.0.3 • Published 4 years ago

invokable v1.0.3

Weekly downloads
19
License
MIT
Repository
github
Last release
4 years ago

invokable

A way to make invokable JavaScript objects. Reminiscent of Python's __call__.

Installation

npm install invokable

Usage

Using with a class:

import {Invokable} from 'invokable';

class Rect {
  constructor(width, height) {
    this.width = width;
    this.height = height;
    return Invokable.create(this);
  }

  get area() {
    return this.width * this.height;
  }

  [Invokable.call](depth = 1) {
    return this.area * depth;
  }
}
  • Implement a method with the computed name [Invokable.call] with any signature of your choice.
  • End the constructor with return Invokable.create(this)

Also works with a plain object:

import {Invokable} from 'invokable';

const Rect = (width, height) =>
  Invokable.create({
    width,
    height,
    get area() {
      return this.width * this.height;
    },
    [Invokable.call](depth = 1) {
      return this.area * depth;
    },
  });
  • Declare a property with the computed name [Invokable.call] with any function of your choice.
  • Pass the entire object to Invokable.create

Capabilities and Limitations

Invokable.create can:

  • Preserve the this context of the object.
  • Do the right thing when an object's [Invokable.call] method has been replaced at runtime.
  • Replicate all properties without triggering getters and setters.
  • Inherit the same prototype, ensuring things like constructor and the instanceof operator still work.
  • Create functions whose name property is writable. Simply define a name property in the original target object.

It cannot:

  • Modify the original object. A new object that masquerades as the original is returned.
  • Do anything with an object that does not implement [Invokable.call]. A TypeError is thrown when such an object is given.
  • Work on JavaScript engines that don't support Object.setPrototypeOf, Object.getPrototypeOf, Object.getOwnPropertyDescriptors, and Object.defineProperties, unless they have been polyfilled.
  • Be rebound using .bind(...). However, the [Invokable.call] method can still be rebound.

API

  • Invokable.create(target) takes a target object that conforms to the Invokable interface and returns a new object that is a function that masquerades as the original target object. All properties, values, getters, setters, or otherwise are replicated, including the prototype.

    Normal function objects have a read-only property called name. However, if the target object defines its own value or getter called name, then the name property will be made writable in the result. In all cases, the name property will default to the original function or surrounding class name.

    If the target object does not implement [Invokable.call], a TypeError is thrown.

  • Invokable.call is a tag that denotes callability. It is a constant used by an object to conform to the Invokable interface. Its current value is the string __call__, but it may become a symbol in the future.

TypeScript Support

TypeScript support comes out of the box without any additional setup. Here is the TypeScript version of the class example:

import {Invokable} from 'invokable';

class Rect {
  constructor(public width: number, public height: number) {
    return Invokable.create(this);
  }

  get area() {
    return this.width * this.height;
  }

  [Invokable.call](depth = 1) {
    return this.area * depth;
  }
}

interface Rect {
  (depth?: number): number;
}

Note that it is necessary to modify the interface generated by the class in order make class instances callable to the type system.

The TypeScript version of the plain object example:

import {Invokable} from 'invokable';

const Rect = (width: number, height: number) =>
  Invokable.create({
    width,
    height,
    get area() {
      return this.width * this.height;
    },
    [Invokable.call](depth = 1) {
      return this.area * depth;
    },
  });

Performance

The following observations are based on the benchmarks listed further below.

  • Creating an invokable object is very slow. Doing so for a plain object incurs around a 7x slowdown, whereas doing so for a class instance can suffer from around a 700x slowdown. Therefore, avoid performing a huge number of calls to Invokable.create in a hot code path.
  • Property access, regardless of whether or not through the prototype, is not significantly impacted.
  • Invoking the invokable object itself is also not guaranteed to be faster than directly invoking the method it points to.

Inspiration

invokable is inspired by the callable-object project.

Benchmarks

To run these on your machine, run npm run bench. The process typically takes a few minutes, and directly writes results to STDOUT as they finish running.

Results were measured on an Intel Core M @ 1.2GHz with 8GB of DDR3-1600 on Node.JS v8.3.0. Throughput numbers are expressed in operations per second (ops).

Class instantiation, no args

Test descriptionThroughputErrorPercent of best
plain107464004±1.71%100.00%
invokable147857±3.32%0.14%

Class instantiation, 5 args

Test descriptionThroughputErrorPercent of best
plain12722162±1.78%100.00%
invokable144240±3.45%1.13%

Class instantiation, 10 args

Test descriptionThroughputErrorPercent of best
plain9185841±1.48%100.00%
invokable144790±3.58%1.58%

Class instance: own property access

Test descriptionThroughputErrorPercent of best
plain450120750±1.48%100.00%
invokable448748906±1.51%99.70%

Class instance: method call on prototype

Test descriptionThroughputErrorPercent of best
plain443185849±1.68%98.26%
invokable451041317±1.64%100.00%

Class instance: getter on prototype

Test descriptionThroughputErrorPercent of best
plain448271681±2.17%100.28%
invokable447015599±1.31%100.00%

Class instance: setter on prototype

Test descriptionThroughputErrorPercent of best
plain356063076±3.93%96.11%
invokable370482861±1.55%100.00%

Object creation, no args

Test descriptionThroughputErrorPercent of best
plain495464±5.21%100.00%
invokable73149±3.58%14.76%

Object creation, 5 args

Test descriptionThroughputErrorPercent of best
plain455371±8.85%100.00%
invokable71283±3.99%15.65%

Object creation, 10 args

Test descriptionThroughputErrorPercent of best
plain470126±6.42%100.00%
invokable70540±4.37%15.00%

Object: own property access

Test descriptionThroughputErrorPercent of best
plain416151276±0.96%99.76%
invokable417164274±0.87%100.00%

Object: own property call

Test descriptionThroughputErrorPercent of best
plain83609556±2.94%100.00%
invokable54168693±1.27%64.79%

Object: own property getter

Test descriptionThroughputErrorPercent of best
plain51863522±1.21%100.00%
invokable51401432±5.03%99.11%

Object: own property setter

Test descriptionThroughputErrorPercent of best
plain267193097±0.85%100.00%
invokable197801204±18.43%74.03%

Invocation, no args

Test descriptionThroughputErrorPercent of best
instance.__call__()17807793±1.19%95.57%
instance[Invokable.call]()17446234±1.55%93.63%
instance()18633571±1.51%100.00%

Invocation, 5 args

Test descriptionThroughputErrorPercent of best
instance.__call__()8164700±3.61%97.58%
instance[Invokable.call]()8367181±1.47%100.00%
instance()7318022±5.51%87.46%

Invocation, 10 args

Test descriptionThroughputErrorPercent of best
instance.__call__()6898069±2.00%94.93%
instance[Invokable.call]()7040076±3.05%96.88%
instance()7266453±2.24%100.00%