├── .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 [](https://travis-ci.org/zkat/protocols) [](https://npm.im/@zkat/protocols) [](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 |
--------------------------------------------------------------------------------