diff --git a/README.md b/README.md index 6b51dc4..ca7527b 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ in a clean and consitent fashion. Nothing more, and nothing less. * [`slugify`(text: String [, separator: String = '-']): String](#slugifytext-string--separator-string----string) * [`typecast`(text: String): Object](#typecasttext-string-object) * [`typeOf`(element: `any`): String](#typeofelement-any-string) + * [`Callable`: Class](#callable-class) * [Authors](#authors) * [Contributors ✨](#contributors-) @@ -376,6 +377,32 @@ typeOf(() => {}) // function typeOf(new Set()) // set ``` +### `Callable`: [Class][] + +A class object whose instances are derived from [Function][] and can be called. +When exteded, a [Symbol][] function defined by `Symbol.for('call')` will be executed +with any arguments that were passed + +##### Example + +```javascript +const {Callable} = require('@logdna/stdlib') +const __call__ = Symbol.for('call') +class Hello extends Callable { + constructor(opts) { + this.yell = !!opts.yell + } + [__call__](name) { + const output = `Hello, ${name}` + console.log(this.yell ? `${output.toUpperCase()}!` : output) + } +} + +const screamAt = new Hello({yell: true}) + +screamAt('bill') // HELLO, BILL! +``` + ## Authors * [**Eric Satterwhite**](mailto:eric.satterwhite@logdna.com) <eric.satterwhite@logdna.com> @@ -388,6 +415,8 @@ typeOf(new Set()) // set [Number]: https://mdn.io/number [Object]: https://mdn.io/object [Function]: https://mdn.io/function +[Class]: https://mdn.io/class +[Symbol]: https://mdn.io/symbol [Generator]: https://mdn.io/generator [itertools]: https://docs.python.org/3.7/library/itertools.html diff --git a/lib/callable.js b/lib/callable.js new file mode 100644 index 0000000..67e69a2 --- /dev/null +++ b/lib/callable.js @@ -0,0 +1,42 @@ +'use strict' +/** + * @module lib/callable + * @author Eric Satterwhite + **/ + +/** + * A class object whose instances are also callable + * To define a callable object, extend this class and gif it a symbol function + * using Symbol.for("call") + * @constructor + * @alias module:lib/callable + * @extends Function + * @example + * class Message extends Callable { + * constructor(name) { + * super() + * this.name = name + * } + * + * [__call__]() { + * return `Hello ${this.name}` + * } + * } + * + * const bob = new Message('bob') + * bob.name // bob + * bob() // Hello bob + **/ +class Callable extends Function { + get [Symbol.toStringTag]() { + return 'Callable' + } + + constructor() { + super('...args', 'return this._bound[Symbol.for("call")](...args)') + this._bound = this.bind(this) + return this._bound + } +} + +module.exports = Callable diff --git a/test/unit/callable.js b/test/unit/callable.js new file mode 100644 index 0000000..890f229 --- /dev/null +++ b/test/unit/callable.js @@ -0,0 +1,23 @@ +'use strict' + +const {test, threw} = require('tap') +const typeOf = require('../../lib/type-of.js') +const Callable = require('../../lib/callable.js') + +test('callable', async (t) => { + t.test('string representation', async (t) => { + t.equal(typeOf(new Callable()), 'callable', 'identifies as callable') + }) + + t.test('extensbility', async (t) => { + const __call__ = Symbol.for('call') + class Sum extends Callable { + [__call__](a, b = 1) { + return a + b + } + } + + const sum = new Sum() + t.equal(sum(10, 3), 13, 'call function executed') + }) +}).catch(threw)