├── .gitignore ├── .travis.yml ├── README.md ├── index.js ├── package.json └── test ├── basic.js └── method-style.js /.gitignore: -------------------------------------------------------------------------------- 1 | /.nyc_output 2 | /node_modules/ 3 | /coverage 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - "6" 5 | - "4.2" 6 | - "0.12" 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Protocols [![Travis](https://img.shields.io/travis/zkat/protocols.svg)](https://travis-ci.org/zkat/protocols) [![npm version](https://img.shields.io/npm/v/@zkat/protocols.svg)](https://npm.im/@zkat/protocols) [![license](https://img.shields.io/npm/l/@zkat/protocols.svg)](https://npm.im/@zkat/protocols) 2 | 3 | [`@zkat/protocols`](https://github.com/zkat/protocols) is a JavaScript library 4 | is a library for making groups of methods, called "protocols", that work 5 | together to provide some abstract functionality that other things can then rely 6 | on. If you're familiar with the concept of ["duck 7 | typing"](https://en.wikipedia.org/wiki/Duck_typing), then it might make sense to 8 | think of protocols as things that explicitly define what methods you need in 9 | order to "clearly be a duck". 10 | 11 | On top of providing a nice, clear interface for defining these protocols, this 12 | module clear, useful errors when implementations are missing something or doing 13 | something wrong. 14 | 15 | One thing that sets this library apart from others is that on top of defining 16 | duck-typed protocols on a single class/type, it lets you have different 17 | implementations depending on the _arguments_. So a method on `Foo` may call 18 | different code dependent on whether its first _argument_ is `Bar` or `Baz`. If 19 | you've ever wished a method worked differently for different types of things 20 | passed to it, this does that! 21 | 22 | ## Install 23 | 24 | `$ npm install @zkat/protocols` 25 | 26 | ## Table of Contents 27 | 28 | * [Example](#example) 29 | * [API](#api) 30 | * [`protocol()`](#protocol) 31 | * [`implementation`](#impl) 32 | 33 | ### Example 34 | 35 | ```javascript 36 | import protocol from "@zkat/protocols" 37 | 38 | // Quackable is a protocol that defines three methods 39 | const Quackable = protocol({ 40 | walk: [], 41 | talk: [], 42 | isADuck: [] 43 | }) 44 | 45 | // `duck` must implement `Quackable` for this function to work. It doesn't 46 | // matter what type or class duck is, as long as it implements Quackable. 47 | function doStuffToDucks (duck) { 48 | if (!duck.isADuck()) { 49 | throw new Error('I want a duck!') 50 | } else { 51 | console.log(duck.walk()) 52 | console.log(duck.talk()) 53 | } 54 | } 55 | 56 | // elsewhere in the project... 57 | class Person () {} 58 | 59 | Quackable(Person, { 60 | walk() { return "my knees go the wrong way but I'm doing my best" } 61 | talk() { return "uhhh... do I have to? oh... 'Quack' 😒"} 62 | isADuck() { return true /* lol I'm totally lying */ } 63 | }) 64 | 65 | // and another place... 66 | class Duck () {} 67 | 68 | Quackable(Duck, { 69 | walk() { return "*hobble hobble*" } 70 | talk() { return "QUACK QUACK" } 71 | isADuck() { return true } 72 | }) 73 | 74 | // main.js 75 | doStuffToDucks(new Person()) // works 76 | doStuffToDucks(new Duck()) // works 77 | doStuffToDucks({ walk() { return 'meh' } }) // => error 78 | ``` 79 | 80 | ### API 81 | 82 | #### `protocol(?, )` 83 | 84 | Defines a new protocol on across arguments of types defined by ``, which 85 | will expect implementations for the functions specified in ``. 86 | 87 | If `` is missing, it will be treated the same as if it were an empty 88 | array. 89 | 90 | 91 | The types in `` must map, by string name, to the type names specified in 92 | ``, or be an empty array if `` is omitted. The types in `` 93 | will then be used to map between method implementations for the individual 94 | functions, and the provided types in the impl. 95 | 96 | ##### Example 97 | 98 | ```javascript 99 | const Eq = protocol(['a', 'b'], { 100 | eq: ['a', 'b'] 101 | }) 102 | ``` 103 | 104 | #### `proto(?, ?, )` 105 | 106 | Adds a new implementation to the given `proto` across ``. 107 | 108 | `` must be an object with functions matching the protocol's 109 | API. The types in `` will be used for defining specific methods using 110 | the function as the body. 111 | 112 | Protocol implementations must include either ``, ``, or both: 113 | 114 | * If only `` is present, implementations will be defined the same as 115 | "traditional" methods -- that is, the definitions in `` 116 | will add function properties directly to ``. 117 | 118 | * If only `` is present, the protocol will keep all protocol functions as 119 | "static" methods on the protocol itself. 120 | 121 | * If both are specified, protocol implementations will add methods to the ``, and define multimethods using ``. 122 | 123 | If a protocol is derivable -- that is, all its functions have default impls, 124 | then the `` object can be omitted entirely, and the protocol 125 | will be automatically derived for the given `` 126 | 127 | ##### Example 128 | 129 | ```javascript 130 | import protocol from '@zkat/protocols' 131 | 132 | // Singly-dispatched protocols 133 | const Show = protocol({ 134 | show: [] 135 | }) 136 | 137 | class Foo {} 138 | 139 | Show(Foo, { 140 | show () { return `[object Foo(${this.name})]` } 141 | }) 142 | 143 | var f = new Foo() 144 | f.name = 'alex' 145 | f.show() === '[object Foo(alex)]' 146 | ``` 147 | 148 | ```javascript 149 | import protocol from '@zkat/protocols' 150 | 151 | // Multi-dispatched protocols 152 | const Comparable = protocol(['target'], { 153 | compare: ['target'], 154 | }) 155 | 156 | class Foo {} 157 | class Bar {} 158 | class Baz {} 159 | 160 | Comparable(Foo, [Bar], { 161 | compare (bar) { return 'bars are ok' } 162 | }) 163 | 164 | Comparable(Foo, [Baz], { 165 | compare (baz) { return 'but bazzes are better' } 166 | }) 167 | 168 | const foo = new Foo() 169 | const bar = new Bar() 170 | const baz = new Baz() 171 | 172 | foo.compare(bar) // 'bars are ok' 173 | foo.compare(baz) // 'but bazzes are better' 174 | ``` 175 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var genfun = require('genfun') 4 | 5 | var Protocol = module.exports = function (types, spec, opts) { 6 | if (Object.getPrototypeOf(types) !== Array.prototype) { 7 | // protocol(spec, opts?) syntax for method-based protocols 8 | opts = spec 9 | spec = types 10 | types = [] 11 | } 12 | var proto = function (target, types, impls) { 13 | return Protocol.impl(proto, target, types, impls) 14 | } 15 | proto._metaobject = opts && opts.metaobject 16 | proto._types = types 17 | proto._defaultImpls = {} 18 | proto._gfTypes = {} 19 | proto._derivable = true 20 | proto._methodNames = Object.keys(spec) 21 | proto._methodNames.forEach(function (name) { 22 | proto[name] = proto._metaobject 23 | ? Protocol.meta.createGenfun(proto._metaobject, proto, null, name) 24 | : _metaCreateGenfun(null, proto, null, name) 25 | var gfTypes = spec[name] 26 | // genfun specs can have a fn attached to the end as a default impl 27 | if (typeof gfTypes[gfTypes.length - 1] === 'function') { 28 | proto._defaultImpls[name] = gfTypes.pop() 29 | } else { 30 | proto._derivable = false 31 | } 32 | proto._gfTypes[name] = gfTypes.map(function (typeId) { 33 | var idx = proto._types.indexOf(typeId) 34 | if (idx === -1) { 35 | throw new Error('type `' + typeId + '` for function `' + name + 36 | '` does not match any protocol types') 37 | } else { 38 | return idx 39 | } 40 | }) 41 | }) 42 | return proto 43 | } 44 | 45 | Protocol.noImplFound = genfun.noApplicableMethod 46 | 47 | function typeName (obj) { 48 | return (/\[object ([a-zA-Z0-9]+)\]/) 49 | .exec(({}).toString.call(obj))[1] 50 | } 51 | 52 | function installMethodErrorMessage (proto, gf, target, name) { 53 | Protocol.noImplFound.add([gf], function (gf, thisArg, args) { 54 | var msg = 55 | 'No ' + (proto.name || 'protocol') + ' impl for `' + 56 | name + 57 | '` found for arguments of types: (' + 58 | [].map.call(args, typeName).join(', ') + ')' 59 | if (target) { 60 | msg += ' and `this` type ' + typeName(thisArg) 61 | } 62 | var err = new Error(msg) 63 | err.protocol = proto 64 | err.function = gf 65 | err.thisArg = thisArg 66 | err.args = args 67 | throw err 68 | }) 69 | } 70 | 71 | Protocol.isDerivable = function (proto) { return proto._derivable } 72 | 73 | Protocol.impl = function (proto, target, types, implementations) { 74 | if (Object.getPrototypeOf(target) === Array.prototype) { 75 | // Proto([Array], { map() { ... } }) 76 | implementations = types 77 | types = target 78 | target = null 79 | } else if (types && Object.getPrototypeOf(types) !== Array.prototype) { 80 | // Proto(Array, { map() { ... } }) 81 | implementations = types 82 | types = [] 83 | } 84 | if (typeof target === 'function') { 85 | target = target.prototype 86 | } 87 | if (!implementations && proto._derivable) { 88 | implementations = proto._defaultImpls 89 | } 90 | Object.keys(proto).forEach(function (name) { 91 | if (name[0] !== '_' && 92 | !implementations[name] && 93 | !proto._defaultImpls[name]) { 94 | throw new Error('missing implementation for `' + name + '`') 95 | } 96 | }) 97 | var pTypes = proto._types 98 | if (types.length > pTypes.length) { 99 | throw new Error('protocol expects to be defined across at least ' + 100 | pTypes.length + ' types, but ' + types.length + 101 | ' were specified.') 102 | } else if (types.length < pTypes.length) { 103 | for (var i = 0; i < pTypes.length - types.length; i++) { 104 | types.push(Object) 105 | } 106 | } 107 | Object.keys(implementations).forEach(function (name) { 108 | if (proto._methodNames.indexOf(name) === -1) { 109 | throw new Error('`' + name + '` is not part of the protocol') 110 | } 111 | }) 112 | proto._methodNames.forEach(function (name) { 113 | var fn = implementations[name] || proto._defaultImpls[name] 114 | var methodTypes = calculateMethodTypes(name, proto, types) 115 | if (target && !target[name]) { 116 | target[name] = proto._metaobject 117 | ? Protocol.meta.createGenfun(proto._metaobject, proto, target, name) 118 | : _metaCreateGenfun(null, proto, target, name) 119 | } 120 | 121 | proto._metaobject 122 | ? Protocol.meta.addMethod(proto._metaobject, proto, target, name, methodTypes, fn) 123 | : _metaAddMethod(null, proto, target, name, methodTypes, fn) 124 | }) 125 | } 126 | 127 | function calculateMethodTypes (name, proto, types) { 128 | return proto._gfTypes[name].map(function (typeIdx) { 129 | return types[typeIdx] 130 | }) 131 | } 132 | 133 | // MOP 134 | function _metaCreateGenfun (_mo, proto, target, name) { 135 | var gf = genfun() 136 | installMethodErrorMessage(proto, gf, target, name) 137 | return gf 138 | } 139 | function _metaAddMethod (_mo, proto, target, name, methodTypes, fn) { 140 | return (target || proto)[name].add(methodTypes, fn) 141 | } 142 | 143 | Protocol.meta = Protocol(['a'], { 144 | createGenfun: ['a'], 145 | addMethod: ['a'] 146 | }) 147 | 148 | Protocol.meta([], { 149 | createGenfun: _metaCreateGenfun, 150 | addMethod: _metaAddMethod 151 | }) 152 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@zkat/protocols", 3 | "version": "2.0.1", 4 | "description": "Multi-type protocol-based polymorphism", 5 | "main": "index.js", 6 | "files": [ 7 | "index.js" 8 | ], 9 | "scripts": { 10 | "preversion": "npm t", 11 | "test": "standard && nyc -- mocha --reporter spec" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/zkat/protocols.git" 16 | }, 17 | "keywords": [ 18 | "oop", 19 | "util", 20 | "object", 21 | "oriented", 22 | "protocols", 23 | "multimethod", 24 | "clojure", 25 | "generic", 26 | "functions", 27 | "clos", 28 | "polymorphism", 29 | "impl", 30 | "typeclass", 31 | "traits" 32 | ], 33 | "author": "Kat Marchán ", 34 | "license": "CC0-1.0", 35 | "bugs": { 36 | "url": "https://github.com/zkat/protocols/issues" 37 | }, 38 | "homepage": "https://github.com/zkat/protocols#readme", 39 | "dependencies": { 40 | "genfun": "^3.0.0" 41 | }, 42 | "devDependencies": { 43 | "mocha": "^3.0.2", 44 | "nyc": "^8.1.0", 45 | "standard": "^8.0.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | var assert = require('assert') 3 | var protocol = require('../') 4 | 5 | describe('types', function () { 6 | var Eq = protocol(['a', 'b'], { 7 | eq: ['a', 'b'], 8 | neq: ['b', 'a'] 9 | }) 10 | it('collects the protocol types', function () { 11 | assert.deepEqual(Eq._types, ['a', 'b']) 12 | }) 13 | it('collects the genfun types and their positions', function () { 14 | assert.deepEqual(Eq._gfTypes, { 15 | eq: [0, 1], 16 | neq: [1, 0] 17 | }) 18 | }) 19 | it('errors if a typespec is invalid', function () { 20 | assert.throws(function () { 21 | protocol(['a'], { 22 | x: ['b'] 23 | }, /type `b` for function `x` does not match any protocol types/) 24 | }) 25 | }) 26 | }) 27 | 28 | describe('derivation', function () { 29 | describe('isDerivable', function () { 30 | var Eq = protocol(['a', 'b'], { 31 | eq: ['a', 'b', function (a, b) { return a === b }], 32 | neq: ['a', 'b', function (a, b) { return !Eq.eq(a, b) }] 33 | }) 34 | it('allows derivation if all functions have default impls', function () { 35 | assert.ok(protocol.isDerivable(Eq)) 36 | Eq([Number, Number]) 37 | assert.ok(Eq.eq(1, 1)) 38 | assert.ok(Eq.neq(2, 3)) 39 | }) 40 | 41 | var Show = protocol(['data', 'exemplar'], { 42 | show: ['data', 'exemplar'], 43 | meh: ['data', function () {}] 44 | }) 45 | it('disallows derivation if any of the gfs have no defaults', function () { 46 | assert.ok(!protocol.isDerivable(Show)) 47 | }) 48 | }) 49 | }) 50 | 51 | describe('implementations', function () { 52 | describe('protocol implementation', function () { 53 | it('defines implementations for protocol functions', function () { 54 | var Eq = protocol(['a', 'b'], { 55 | eq: ['a', 'b'] 56 | }) 57 | Eq([Number, Number], { 58 | eq: function (a, b) { return a === b } 59 | }) 60 | assert.ok(Eq.eq(1, 1)) 61 | assert.throws(function () { 62 | Eq.eq({}, {}) 63 | }, /no protocol impl/i) 64 | }) 65 | }) 66 | it('fails if no matching implementation', function () { 67 | var Eq = protocol(['a'], { eq: ['a', 'a'] }) 68 | assert.throws(function () { 69 | Eq.eq(1, 1) 70 | }, /no protocol impl/i) 71 | }) 72 | it('errors if too many types specified', function () { 73 | var Eq = protocol(['a', 'b'], { 74 | eq: ['a', 'b'] 75 | }) 76 | assert.throws(function () { 77 | Eq([Number, String, Number], { eq: function () {} }) 78 | }) 79 | }) 80 | it('treats missing types in impls as Object', function () { 81 | var Foo = protocol(['a', 'b'], { 82 | frob: ['a', 'b'] 83 | }) 84 | Foo([Number], { frob: function (n, anything) { return n + anything } }) 85 | assert.equal(Foo.frob(1, 'two'), '1two') 86 | assert.equal(Foo.frob(1, 2), 3) 87 | assert.throws(function () { 88 | Foo.frob('str', 1) 89 | }, /no protocol impl/i) 90 | }) 91 | it('errors if an extra function is implemented', function () { 92 | var Eq = protocol(['a'], { eq: ['a', 'a'] }) 93 | assert.throws(function () { 94 | Eq([Number], { eq: function () {}, extra: function () {} }) 95 | }, /`extra` is not part of the protocol/i) 96 | }) 97 | it('errors if a function without a default is not implemented', function () { 98 | var Eq = protocol(['a'], { eq: ['a', 'a'] }) 99 | assert.throws(function () { 100 | Eq([Number], { }) 101 | }, /missing implementation for `eq`/i) 102 | }) 103 | }) 104 | -------------------------------------------------------------------------------- /test/method-style.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | var assert = require('assert') 3 | var protocol = require('../') 4 | 5 | describe('method-style protocols', function () { 6 | it('allows typeless definitions', function () { 7 | assert.doesNotThrow(function () { 8 | protocol({ map: [] }) 9 | }) 10 | }) 11 | it('adds genfuns as methods if a target is specified', function () { 12 | var p = protocol({ map: [] }) 13 | var obj1 = {} 14 | var obj2 = {} 15 | p(obj1, { map: function () { return 'one' } }) 16 | p(obj2, { map: function () { return 'two' } }) 17 | assert.equal(obj1.map(), 'one') 18 | assert.equal(obj2.map(), 'two') 19 | }) 20 | it('uses "default" genfun objects when only types specified', function () { 21 | var p = protocol(['f'], { map: ['f'] }) 22 | var obj = {} 23 | p(obj, [Function], { map: function (f) { return 'method' } }) 24 | p([Function], { map: function (f) { return 'plain' } }) 25 | assert.equal(obj.map(function () {}), 'method') 26 | assert.equal(p.map(function () {}), 'plain') 27 | }) 28 | }) 29 | --------------------------------------------------------------------------------