Comparing version 3.3.2 to 4.0.0
447
index.js
'use strict' | ||
var genfun = require('genfun') | ||
const genfun = require('genfun') | ||
var Protocol = module.exports = function (types, spec, opts) { | ||
if (!isArray(types)) { | ||
// protocol(spec, opts?) syntax for method-based protocols | ||
opts = spec | ||
spec = types | ||
types = [] | ||
class Duck extends Function { | ||
// Duck.impl(Foo, [String, Array], { frob (str, arr) { ... }}) | ||
impl (target, types, impls) { | ||
if (!impls && !isArray(types)) { | ||
impls = types | ||
types = [] | ||
} | ||
if (!impls && this.isDerivable) { | ||
impls = this._defaultImpls | ||
} | ||
if (!impls) { | ||
impls = {} | ||
} | ||
if (typeof target === 'function' && !target.isGenfun) { | ||
target = target.prototype | ||
} | ||
checkImpls(this, target, impls) | ||
checkArgTypes(this, types) | ||
this._constraints.forEach(c => { | ||
if (!c.verify(target, types)) { | ||
throw new Error(`Implementations of ${ | ||
this.name || 'this protocol' | ||
} must first implement ${ | ||
c.parent.name || 'its constraint protocols defined in opts.where.' | ||
}`) | ||
} | ||
}) | ||
this._methodNames.forEach(name => { | ||
defineMethod(this, name, target, types, impls) | ||
}) | ||
} | ||
var proto = function (target, types, impls) { | ||
return Protocol.impl(proto, target, types, impls) | ||
} | ||
proto._metaobject = opts && opts.metaobject | ||
proto._types = types | ||
proto._defaultImpls = {} | ||
proto._gfTypes = {} | ||
proto._derivable = true | ||
proto._methodNames = Object.keys(spec) | ||
proto._methodNames.forEach(function (name) { | ||
proto[name] = proto._metaobject | ||
? Protocol.meta.createGenfun(proto._metaobject, proto, null, name) | ||
: _metaCreateGenfun(null, proto, null, name) | ||
var gfTypes = spec[name] | ||
// genfun specs can have a fn attached to the end as a default impl | ||
if (typeof gfTypes[gfTypes.length - 1] === 'function') { | ||
proto._defaultImpls[name] = gfTypes.pop() | ||
} else { | ||
proto._derivable = false | ||
hasImpl (arg, args) { | ||
args = args || [] | ||
const fns = this._methodNames | ||
var gf | ||
if (typeof arg === 'function' && !arg.isGenfun) { | ||
arg = arg.prototype | ||
} | ||
proto._gfTypes[name] = gfTypes.map(function (typeId) { | ||
var idx = proto._types.indexOf(typeId) | ||
if (idx === -1) { | ||
throw new Error('type `' + typeId + '` for function `' + name + | ||
'` does not match any protocol types') | ||
args = args.map(arg => { | ||
if (typeof arg === 'function' && !arg.isGenfun) { | ||
return arg.prototype | ||
} else { | ||
return idx | ||
return arg | ||
} | ||
}) | ||
}) | ||
return proto | ||
for (var i = 0; i < fns.length; i++) { | ||
gf = arg[fns[i]] | ||
if (!gf || | ||
(gf.hasMethod | ||
? !gf.hasMethod.apply(gf, args) | ||
: typeof gf === 'function')) { | ||
return false | ||
} | ||
} | ||
return true | ||
} | ||
// MyDuck.matches('a', ['this', 'c']) | ||
matches (thisType, argTypes) { | ||
if (!argTypes && isArray(thisType)) { | ||
argTypes = thisType | ||
thisType = 'this' | ||
} | ||
if (!thisType) { | ||
thisType = 'this' | ||
} | ||
if (!argTypes) { | ||
argTypes = [] | ||
} | ||
return new Constraint(this, thisType, argTypes) | ||
} | ||
} | ||
Duck.prototype.isDuck = true | ||
Duck.prototype.isProtocol = true | ||
Protocol.noImplFound = genfun.noApplicableMethod | ||
const Protoduck = module.exports = define(['duck'], { | ||
createGenfun: ['duck', _metaCreateGenfun], | ||
addMethod: ['duck', _metaAddMethod] | ||
}, {name: 'Protoduck'}) | ||
function typeName (obj) { | ||
return (/\[object ([a-zA-Z0-9]+)\]/) | ||
.exec(({}).toString.call(obj))[1] | ||
} | ||
const noImplFound = module.exports.noImplFound = genfun.noApplicableMethod | ||
function installMethodErrorMessage (proto, gf, target, name) { | ||
Protocol.noImplFound.add([gf], function (gf, thisArg, args) { | ||
var msg = | ||
'No ' + (proto.name || 'protocol') + ' impl for `' + | ||
(target ? typeName(thisArg) + '#' : '') + | ||
name + | ||
'(' + | ||
[].map.call(args, typeName).join(', ') + ')`' | ||
msg += '\n\n' | ||
msg += 'You must implement ' | ||
msg += (proto.name || 'the protocol `' + name + '` belongs to') | ||
msg += ' in order to call `' + name + '` with these arguments.\n' | ||
var err = new Error(msg) | ||
err.protocol = proto | ||
err.function = gf | ||
err.thisArg = thisArg | ||
err.args = args | ||
throw err | ||
module.exports.define = define | ||
function define (types, spec, opts) { | ||
if (!isArray(types)) { | ||
// protocol(spec, opts?) syntax for method-based protocols | ||
opts = spec | ||
spec = types | ||
types = [] | ||
} | ||
const duck = function (thisType, argTypes) { | ||
return duck.matches(thisType, argTypes) | ||
} | ||
Object.setPrototypeOf(duck, Duck.prototype) | ||
duck.isDerivable = true | ||
Object.defineProperty(duck, 'name', { | ||
value: (opts && opts.name) || 'Protocol' | ||
}) | ||
if (opts && opts.where) { | ||
let where = opts.where | ||
if (!isArray(opts.where)) { where = [opts.where] } | ||
duck._constraints = where.map(w => w.isProtocol // `where: [Foo]` | ||
? w.matches() | ||
: w | ||
) | ||
} else { | ||
duck._constraints = [] | ||
} | ||
duck.isProtocol = true | ||
duck._metaobject = opts && opts.metaobject | ||
duck._types = types | ||
duck._defaultImpls = {} | ||
duck._gfTypes = {} | ||
duck._methodNames = Object.keys(spec) | ||
duck._methodNames.forEach(name => { | ||
checkMethodSpec(duck, name, spec) | ||
}) | ||
duck._constraints.forEach(c => c.attach(duck)) | ||
return duck | ||
} | ||
Protocol.isDerivable = function (proto) { return proto._derivable } | ||
Protocol.hasImpl = function (proto, arg, args) { | ||
args = args || [] | ||
if (isArray(arg)) { | ||
args = arg | ||
arg = null | ||
function checkMethodSpec (duck, name, spec) { | ||
let gfTypes = spec[name] | ||
if (typeof gfTypes === 'function') { | ||
duck._defaultImpls[name] = gfTypes | ||
gfTypes = [gfTypes] | ||
} if (typeof gfTypes[gfTypes.length - 1] === 'function') { | ||
duck._defaultImpls[name] = gfTypes.pop() | ||
} else { | ||
duck.isDerivable = false | ||
} | ||
var fns = proto._methodNames | ||
var gf | ||
for (var i = 0; i < fns.length; i++) { | ||
if (arg) { | ||
gf = arg[fns[i]] | ||
duck._gfTypes[name] = gfTypes.map(typeId => { | ||
const idx = duck._types.indexOf(typeId) | ||
if (idx === -1) { | ||
throw new Error( | ||
`type '${ | ||
typeId | ||
}' for function '${ | ||
name | ||
}' does not match any protocol types (${ | ||
duck._types.join(', ') | ||
}).` | ||
) | ||
} else { | ||
gf = proto[fns[i]] | ||
return idx | ||
} | ||
if (!gf || | ||
(gf.hasMethod | ||
? !gf.hasMethod.apply(gf, args) | ||
: typeof gf === 'function')) { | ||
return false | ||
} | ||
} | ||
return true | ||
}) | ||
} | ||
Protocol.impl = function (proto, target, types, implementations) { | ||
if (isArray(target)) { | ||
// Proto([Array], { map() { ... } }) | ||
implementations = types | ||
types = target | ||
target = null | ||
} else if (types && !isArray(types)) { | ||
// Proto(Array, { map() { ... } }) | ||
implementations = types | ||
types = [] | ||
function defineMethod (duck, name, target, types, impls) { | ||
const methodTypes = duck._gfTypes[name].map(function (typeIdx) { | ||
return types[typeIdx] | ||
}) | ||
for (let i = methodTypes.length - 1; i >= 0; i--) { | ||
if (methodTypes[i] === undefined) { | ||
methodTypes.pop() | ||
} else { | ||
break | ||
} | ||
} | ||
if (typeof target === 'function') { | ||
target = target.prototype | ||
const useMetaobject = duck._metaobject && duck._metaobject !== Protoduck | ||
// `target` does not necessarily inherit from `Object` | ||
if (!Object.prototype.hasOwnProperty.call(target, name)) { | ||
// Make a genfun if there's nothing there | ||
const gf = useMetaobject | ||
? duck._metaobject.createGenfun(duck, target, name, null) | ||
: _metaCreateGenfun(duck, target, name, null) | ||
target[name] = gf | ||
} else if (typeof target[name] === 'function' && !target[name].isGenfun) { | ||
// Turn non-gf functions into genfuns | ||
const gf = useMetaobject | ||
? duck._metaobject.createGenfun(duck, target, name, target[name]) | ||
: _metaCreateGenfun(duck, target, name, target[name]) | ||
target[name] = gf | ||
} | ||
if (!implementations && proto._derivable) { | ||
implementations = proto._defaultImpls | ||
const fn = impls[name] || duck._defaultImpls[name] | ||
if (fn) { // checkImpls made sure this is safe | ||
useMetaobject | ||
? duck._metaobject.addMethod(duck, target, name, methodTypes, fn) | ||
: _metaAddMethod(duck, target, name, methodTypes, fn) | ||
} | ||
Object.keys(proto).forEach(function (name) { | ||
if (name[0] !== '_' && | ||
!implementations[name] && | ||
!proto._defaultImpls[name]) { | ||
throw new Error('missing implementation for `' + name + '`') | ||
} | ||
function checkImpls (duck, target, impls) { | ||
duck._methodNames.forEach(function (name) { | ||
if ( | ||
!impls[name] && | ||
!duck._defaultImpls[name] && | ||
// Existing methods on the target are acceptable defaults. | ||
typeof target[name] !== 'function' | ||
) { | ||
throw new Error(`Missing implementation for ${ | ||
formatMethod(duck, name, duck.name) | ||
}. Make sure the method is present in your ${ | ||
duck.name || 'protocol' | ||
} definition. Required methods: ${ | ||
duck._methodNames.filter(m => { | ||
return !duck._defaultImpls[m] | ||
}).map(m => formatMethod(duck, m)).join(', ') | ||
}.`) | ||
} | ||
}) | ||
var pTypes = proto._types | ||
if (types.length > pTypes.length) { | ||
throw new Error('protocol expects to be defined across at least ' + | ||
pTypes.length + ' types, but ' + types.length + | ||
' were specified.') | ||
} else if (types.length < pTypes.length) { | ||
for (var i = 0; i < pTypes.length - types.length; i++) { | ||
types.push(Object) | ||
Object.keys(impls).forEach(function (name) { | ||
if (duck._methodNames.indexOf(name) === -1) { | ||
throw new Error( | ||
`${name}() was included in the impl, but is not part of ${ | ||
duck.name || 'the protocol' | ||
}. Allowed methods: ${ | ||
duck._methodNames.map(m => formatMethod(duck, m)).join(', ') | ||
}.` | ||
) | ||
} | ||
}) | ||
} | ||
function formatMethod (duck, name, withDuckName) { | ||
return `${ | ||
withDuckName && duck.name ? `${duck.name}#` : '' | ||
}${name}(${duck._gfTypes[name].map(n => duck._types[n]).join(', ')})` | ||
} | ||
function checkArgTypes (duck, types) { | ||
var requiredTypes = duck._types | ||
if (types.length > requiredTypes.length) { | ||
throw new Error( | ||
`${ | ||
duck.name || 'Protocol' | ||
} expects to be defined across ${ | ||
requiredTypes.length | ||
} type${requiredTypes.length > 1 ? 's' : ''}, but ${ | ||
types.length | ||
} ${types.length > 1 ? 'were' : 'was'} specified.` | ||
) | ||
} | ||
Object.keys(implementations).forEach(function (name) { | ||
if (proto._methodNames.indexOf(name) === -1) { | ||
throw new Error('`' + name + '` is not part of the protocol') | ||
} | ||
function typeName (obj) { | ||
return (/\[object ([a-zA-Z0-9]+)\]/).exec(({}).toString.call(obj))[1] | ||
} | ||
function installMethodErrorMessage (proto, gf, target, name) { | ||
noImplFound.add([gf], function (gf, thisArg, args) { | ||
let parent = Object.getPrototypeOf(thisArg) | ||
while (parent && parent[name] === gf) { | ||
parent = Object.getPrototypeOf(parent) | ||
} | ||
}) | ||
proto._methodNames.forEach(function (name) { | ||
var fn = implementations[name] || proto._defaultImpls[name] | ||
var methodTypes = calculateMethodTypes(name, proto, types) | ||
if (target != null && !{}.hasOwnProperty.call(target, name)) { | ||
target[name] = proto._metaobject | ||
? Protocol.meta.createGenfun(proto._metaobject, proto, target, name) | ||
: _metaCreateGenfun(null, proto, target, name) | ||
if (parent && parent[name] && typeof parent[name] === 'function') { | ||
} | ||
proto._metaobject | ||
? Protocol.meta.addMethod(proto._metaobject, proto, target, name, methodTypes, fn) | ||
: _metaAddMethod(null, proto, target, name, methodTypes, fn) | ||
var msg = `No ${typeName(thisArg)} impl for ${ | ||
proto.name ? `${proto.name}#` : '' | ||
}${name}(${[].map.call(args, typeName).join(', ')}). You must implement ${ | ||
proto.name | ||
? formatMethod(proto, name, true) | ||
: `the protocol ${formatMethod(proto, name)} belongs to` | ||
} in order to call ${typeName(thisArg)}#${name}(${ | ||
[].map.call(args, typeName).join(', ') | ||
}).` | ||
const err = new Error(msg) | ||
err.protocol = proto | ||
err.function = gf | ||
err.thisArg = thisArg | ||
err.args = args | ||
err.code = 'ENOIMPL' | ||
throw err | ||
}) | ||
} | ||
function calculateMethodTypes (name, proto, types) { | ||
return proto._gfTypes[name].map(function (typeIdx) { | ||
return types[typeIdx] | ||
}) | ||
function isArray (x) { | ||
return Object.prototype.toString.call(x) === '[object Array]' | ||
} | ||
// MOP | ||
function _metaCreateGenfun (_mo, proto, target, name) { | ||
var gf = genfun() | ||
// Metaobject Protocol | ||
Protoduck.impl(Protoduck) // defaults configured by definition | ||
function _metaCreateGenfun (proto, target, name, deflt) { | ||
var gf = genfun({ | ||
default: deflt, | ||
name: `${proto.name ? `${proto.name}#` : ''}${name}` | ||
}) | ||
installMethodErrorMessage(proto, gf, target, name) | ||
gf.protocol = proto | ||
gf.duck = proto | ||
return gf | ||
} | ||
function _metaAddMethod (_mo, proto, target, name, methodTypes, fn) { | ||
return (target || proto)[name].add(methodTypes, fn) | ||
function _metaAddMethod (duck, target, name, methodTypes, fn) { | ||
return target[name].add(methodTypes, fn) | ||
} | ||
Protocol.meta = Protocol(['a'], { | ||
createGenfun: ['a'], | ||
addMethod: ['a'] | ||
}) | ||
// Constraints | ||
class Constraint { | ||
constructor (parent, thisType, argTypes) { | ||
this.parent = parent | ||
this.target = thisType | ||
this.types = argTypes | ||
} | ||
Protocol.meta([], { | ||
createGenfun: _metaCreateGenfun, | ||
addMethod: _metaAddMethod | ||
}) | ||
attach (obj) { | ||
this.child = obj | ||
if (this.target === 'this') { | ||
this.thisIdx = 'this' | ||
} else { | ||
const idx = this.child._types.indexOf(this.target) | ||
if (idx === -1) { | ||
this.thisIdx = null | ||
} else { | ||
this.thisIdx = idx | ||
} | ||
} | ||
this.indices = this.types.map(typeId => { | ||
if (typeId === 'this') { | ||
return 'this' | ||
} else { | ||
const idx = this.child._types.indexOf(typeId) | ||
if (idx === -1) { | ||
return null | ||
} else { | ||
return idx | ||
} | ||
} | ||
}) | ||
} | ||
function isArray (x) { | ||
return Object.prototype.toString.call(x) === '[object Array]' | ||
verify (target, types) { | ||
const thisType = ( | ||
this.thisIdx === 'this' || this.thisIdx == null | ||
) | ||
? target | ||
: types[this.thisIdx] | ||
const parentTypes = this.indices.map(idx => { | ||
if (idx === 'this') { | ||
return target | ||
} else if (idx === 'this') { | ||
return types[this.thisIdx] | ||
} else if (idx === null) { | ||
return Object | ||
} else { | ||
return types[idx] || Object.prototype | ||
} | ||
}) | ||
return this.parent.hasImpl(thisType, parentTypes) | ||
} | ||
} | ||
Constraint.prototype.isConstraint = true |
{ | ||
"name": "protoduck", | ||
"version": "3.3.2", | ||
"version": "4.0.0", | ||
"description": "Fancy duck typing for the most serious of ducks.", | ||
@@ -10,6 +10,9 @@ "main": "index.js", | ||
"scripts": { | ||
"preversion": "npm t", | ||
"postversion": "npm publish && git push --follow-tags", | ||
"prerelease": "npm t", | ||
"postrelease": "npm publish && git push --follow-tags", | ||
"pretest": "standard", | ||
"test": "nyc -- mocha --reporter spec" | ||
"release": "standard-version -s", | ||
"test": "tap -J --coverage test/*.js", | ||
"update-coc": "weallbehave -o . && git add CODE_OF_CONDUCT.md && git commit -m 'docs(coc): updated CODE_OF_CONDUCT.md'", | ||
"update-contrib": "weallcontribute -o . && git add CONTRIBUTING.md && git commit -m 'docs(contributing): updated CONTRIBUTING.md'" | ||
}, | ||
@@ -30,2 +33,4 @@ "repository": { | ||
"clojure", | ||
"haskell", | ||
"rust", | ||
"generic", | ||
@@ -46,9 +51,13 @@ "functions", | ||
"dependencies": { | ||
"genfun": "^3.2.1" | ||
"genfun": "^4.0.1" | ||
}, | ||
"devDependencies": { | ||
"mocha": "^3.0.2", | ||
"nyc": "^8.1.0", | ||
"standard": "^8.0.0" | ||
"mocha": "^3.2.0", | ||
"nyc": "^10.2.0", | ||
"standard": "^10.0.2", | ||
"standard-version": "^4.0.0", | ||
"tap": "^10.3.2", | ||
"weallbehave": "^1.0.3", | ||
"weallcontribute": "^1.0.8" | ||
} | ||
} |
262
README.md
@@ -1,7 +0,7 @@ | ||
# Protoduck [![Travis](https://img.shields.io/travis/zkat/protoduck.svg)](https://travis-ci.org/zkat/protoduck) [![npm version](https://img.shields.io/npm/v/protoduck.svg)](https://npm.im/protoduck) [![license](https://img.shields.io/npm/l/protoduck.svg)](https://npm.im/protoduck) | ||
# protoduck [![npm version](https://img.shields.io/npm/v/protoduck.svg)](https://npm.im/protoduck) [![license](https://img.shields.io/npm/l/protoduck.svg)](https://npm.im/protoduck) [![Travis](https://img.shields.io/travis/zkat/protoduck.svg)](https://travis-ci.org/zkat/protoduck) [![AppVeyor](https://ci.appveyor.com/api/projects/status/github/zkat/protoduck?svg=true)](https://ci.appveyor.com/project/zkat/protoduck) [![Coverage Status](https://coveralls.io/repos/github/zkat/protoduck/badge.svg?branch=latest)](https://coveralls.io/github/zkat/protoduck?branch=latest) | ||
[`protoduck`](https://github.com/zkat/protoduck) is a JavaScript library is a | ||
library for making groups of methods, called "protocols", that work together to | ||
provide some abstract functionality that other things can then rely on. If | ||
you're familiar with the concept of ["duck | ||
library for making groups of methods, called "protocols". | ||
If you're familiar with the concept of ["duck | ||
typing"](https://en.wikipedia.org/wiki/Duck_typing), then it might make sense to | ||
@@ -11,16 +11,5 @@ think of protocols as things that explicitly define what methods you need in | ||
On top of providing a nice, clear interface for defining these protocols, this | ||
module clear, useful errors when implementations are missing something or doing | ||
something wrong. | ||
One thing that sets this library apart from others is that on top of defining | ||
duck-typed protocols on a single class/type, it lets you have different | ||
implementations depending on the _arguments_. So a method on `Foo` may call | ||
different code dependent on whether its first _argument_ is `Bar` or `Baz`. If | ||
you've ever wished a method worked differently for different types of things | ||
passed to it, this does that! | ||
## Install | ||
`$ npm install protoduck` | ||
`$ npm install -S protoduck` | ||
@@ -34,8 +23,8 @@ ## Table of Contents | ||
* [Defining protocols](#defining-protocols) | ||
* [Simple impls](#simple-impls) | ||
* [Implementations](#protocol-impls) | ||
* [Multiple dispatch](#multiple-dispatch) | ||
* [Static impls](#static-impls) | ||
* [Constraints](#constraints) | ||
* [API](#api) | ||
* [`protocol()`](#protocol) | ||
* [`impls`](#impl) | ||
* [`define()`](#define) | ||
* [`proto.impl()`](#impl) | ||
@@ -45,6 +34,6 @@ ### Example | ||
```javascript | ||
import protocol from "protoduck" | ||
const protoduck = require('protoduck') | ||
// Quackable is a protocol that defines three methods | ||
const Quackable = protocol({ | ||
const Quackable = protoduck.define({ | ||
walk: [], | ||
@@ -66,13 +55,15 @@ talk: [], | ||
// and another place... | ||
// ...In a different package: | ||
const ducks = require('./ducks') | ||
class Duck () {} | ||
// Implement the protocol on the Duck class. | ||
Quackable(Duck, [], { | ||
walk() { return "*hobble hobble*" } | ||
talk() { return "QUACK QUACK" } | ||
ducks.Quackable.impl(Duck, { | ||
walk () { return "*hobble hobble*" } | ||
talk () { return "QUACK QUACK" } | ||
}) | ||
// main.js | ||
doStuffToDucks(new Duck()) // works! | ||
ducks.doStuffToDucks(new Duck()) // works! | ||
``` | ||
@@ -82,9 +73,8 @@ | ||
* Clear, concise protocol definitions and implementations | ||
* Verifies implementations in case methods are missing | ||
* "Static" implementations ("class methods") | ||
* Verifies implementations in case methods are missing or wrong ones added | ||
* Helpful, informative error messages | ||
* Optional default method implementations | ||
* Fresh JavaScript Feel™ -- methods work just like native methods when called | ||
* Methods can dispatch on arguments, not just `this` ([multimethods](npm.im/genfun)) | ||
* Fast, cached multiple dispatch | ||
* Methods can dispatch on arguments, not just `this` ([multimethods](https://npm.im/genfun)) | ||
* Type constraints | ||
@@ -95,23 +85,26 @@ ### Guide | ||
JavaScript comes with its own method definition mechanism: You simply add | ||
regular `function`s as properties to regular objects, and when you do | ||
`obj.method()`, it calls the right code! ES6/ES2015 further extended this by | ||
adding a `class` syntax that allowed this same system to work with more familiar | ||
syntax sugar: `class Foo { method() { ... } }`. | ||
Like most Object-oriented languages, JavaScript comes with its own way of | ||
defining methods: You simply add regular `function`s as properties to regular | ||
objects, and when you do `obj.method()`, it calls the right code! ES6/ES2015 | ||
further extended this by adding a `class` syntax that allowed this same system | ||
to work with more familiar syntax sugar: `class Foo { method() { ... } }`. | ||
`protoduck` is a similar *language extension*: it adds something called | ||
"protocols" to JavaScript. | ||
The point of "protocols" is to have a more explicit definitions of what methods | ||
"go together". That is, a protocol is a description of a type of object your | ||
code interacts with. If someone passes an object into your library, and it fits | ||
your defined protocol, the assumption is that the object will work just as well. | ||
The purpose of protocols is to have a more explicit definitions of what methods | ||
"go together". That is, if you have a type of task, you can group every method | ||
that things definitely need to have under a protocol, and then write your code | ||
using the methods defined there. The assumption is that anything that defines | ||
that group of methods will work with the rest of your code. | ||
Duck typing is a common term for this sort of thing: If it walks like a duck, | ||
and it talks like a duck, then it may as well be a duck, as far as any of our | ||
code is concerned. | ||
And then you can export the protocol itself, and tell your users "if you | ||
implement this protocol for your own objects, they'll work with my code." | ||
Many other languages have similar or identical concepts under different names: | ||
Java's interfaces, Haskell's typeclasses, Rust's traits. Elixir and Clojure both | ||
call them "protocols" as well. | ||
Duck typing is a common term for this: If it walks like a duck, and it talks | ||
like a duck, then it may as well be a duck, as far as any of our code is | ||
concerned. | ||
One big advantage to using these protocols is that they let users define their | ||
own versions of some abstraction, without requiring the type to inherit from | ||
another -- protocols are independent of inheritance, even though they're able to | ||
work together with it. If you've ever found yourself in some sort of inheritance | ||
mess, this is exactly the sort of thing you use to escape it. | ||
@@ -125,7 +118,7 @@ #### Defining Protocols | ||
// import the library first! | ||
import protocol from "protoduck" | ||
const protoduck = require('protoduck') | ||
// `Ducklike` is the name of our protocol. It defines what it means for | ||
// something to be "like a duck", as far as our code is concerned. | ||
const Ducklike = protocol([], { | ||
const Ducklike = protoduck.define([], { | ||
walk: [], // This says that the protocol requires a "walk" method. | ||
@@ -140,3 +133,3 @@ talk: [] // and ducks also need to talk | ||
#### Simple impls | ||
#### Protocol Impls | ||
@@ -148,5 +141,3 @@ The simplest type of definitions for protocols are as regular methods. In this | ||
Implementation syntax is very similar to protocol definitions, but it calls the | ||
protocol itself, instead of `protocol`. It also refers to the type that you want | ||
to implement it *on*: | ||
Implementation syntax is very similar to protocol definitions, using `.impl`: | ||
@@ -157,6 +148,6 @@ ```javascript | ||
// Implementing `Ducklike` for `Dog`s | ||
Ducklike(Dog, [], { | ||
walk() { return '*pads on all fours*' } | ||
talk() { return 'woof woof. I mean "quack" >_>' } | ||
peck(victim) { return 'Can I just bite ' + victim + ' instead?...' } | ||
Ducklike.impl(Dog, [], { | ||
walk () { return '*pads on all fours*' } | ||
talk () { return 'woof woof. I mean "quack" >_>' } | ||
peck (victim) { return 'Can I just bite ' + victim + ' instead?...' } | ||
}) | ||
@@ -186,7 +177,6 @@ ``` | ||
This type of method is called a multimethod, and isn't a core JavaScript | ||
feature. It's something that sets `protoduck` apart, and it can be really | ||
useful! | ||
This type of method is called a multimethod, and is one of the differences | ||
between protoduck and the default `class` syntax. | ||
The way to use it is simple: in the protocol *definitions*, you put matching | ||
To use it: in the protocol *definitions*, you put matching | ||
strings in different spots where those empty arrays were, and when you | ||
@@ -198,4 +188,4 @@ *implement* the protocol, you give the definition the actual types/objects you | ||
```javascript | ||
const Playable = protocol(['friend'], { | ||
playWith: ['friend'] | ||
const Playful = protoduck.define(['friend'], {// <---\ | ||
playWith: ['friend'] // <------------ these correspond to each other | ||
}) | ||
@@ -208,4 +198,4 @@ | ||
// The first protocol is for Cat/Human combination | ||
Playable(Cat, [Human], { | ||
playWith(human) { | ||
Playful.impl(Cat, [Human], { | ||
playWith (human) { | ||
return '*headbutt* *purr* *cuddle* omg ilu, ' + human.name | ||
@@ -216,4 +206,4 @@ } | ||
// And we define it *again* for a different combination | ||
Playable(Cat, [Dog], { | ||
playWith(dog) { | ||
Playful.impl(Cat, [Dog], { | ||
playWith (dog) { | ||
return '*scratches* *hisses* omg i h8 u, ' + dog.name | ||
@@ -232,47 +222,42 @@ } | ||
In general, most implementations you write won't care what types their arguments | ||
are, but when you do? This may end up saving you a lot of trouble and allowing | ||
some tricks you might realize you can do that weren't possible before! | ||
#### Constraints | ||
#### Static impls | ||
Sometimes, you want to have all the functionality of a certain protocol, but you | ||
want to add a few requirements or other bits an pieces. Usually, you would have | ||
to define the entire functionality of the "parent" protocol in your own protocol | ||
in order to pull this off. This isn't very DRY and thus prone to errors, missing | ||
or out-of-sync functionality, or other issues. You could also just tell users | ||
"hey, if you implement this, make sure to implement that", but there's no | ||
guarantee they'll know about it, or know which arguments map to what. | ||
Finally, there's a type of protocol impl that doesn't involve a `this` value at | ||
all: static impls. Some languages might call them "class methods" as well. In | ||
the case of `protoduck`, though, these static methods exist on the protocol | ||
object itself, rather than a regular JavaScript class. | ||
This is where constraints come in: You can define a protocol that expects | ||
anything that implements it to *also* implement one or more "parent" protocols. | ||
Static methods can be really useful when you want to call them as plain old | ||
functions without having to worry about the `this` value. And because | ||
`protoduck` supports multiple dispatch, it means you can get full method | ||
functionality, but with regular functions that don't need `this`: they just | ||
operate on their standard arguments. | ||
Static impls are easy to make: simply omit the first object type and use the | ||
arguments array to define what the methods will be implemented on: | ||
```javascript | ||
const Eq = protocol(['a', 'b'], { | ||
equals: ['a', 'b'] | ||
}) | ||
const equals = Eq.equals // This isn't necessary, but it shows that we | ||
// don't need a `.` to call them, at all! | ||
Eq([Number, Number], { | ||
equals(x, y) { | ||
return x === y | ||
const Show = proto.define({ | ||
// This syntax allows default impls without using arrays. | ||
toString () { | ||
return Object.prototype.toString.call(this) | ||
}, | ||
toJSON () { | ||
return JSON.stringify(this) | ||
} | ||
}) | ||
Eq([Cat, Cat], { | ||
equals(kitty, cat) { | ||
return kitty.name === cat.name | ||
} | ||
const Log = proto.define({ | ||
log () { console.log(this.toString()) } | ||
}, { | ||
where: Show() | ||
// Also valid: | ||
// [Show('this'), Show('a')] | ||
// [Show('this', ['a', 'b'])] | ||
}) | ||
equals(1, 1) // true | ||
equals(1, 2) // false | ||
equals(snookums, reika) // false | ||
equals(walter, walter) // true | ||
equals(1, snookums) // Error! No protocol impl! | ||
// This fails with an error: must implement Show: | ||
Log.impl(MyThing) | ||
// So derive Show first... | ||
Show.impl(MyThing) | ||
// And now it's ok! | ||
Log.impl(MyThing) | ||
``` | ||
@@ -282,3 +267,3 @@ | ||
#### <a name="protocol"></a> `protocol(<types>?, <spec>)` | ||
#### <a name="define"></a> `define(<types>?, <spec>, <opts>)` | ||
@@ -291,34 +276,34 @@ Defines a new protocol on across arguments of types defined by `<types>`, which | ||
The types in `<spec>` must map, by string name, to the type names specified in | ||
`<types>`, or be an empty array if `<types>` is omitted. The types in `<spec>` | ||
will then be used to map between method implementations for the individual | ||
functions, and the provided types in the impl. | ||
The types in `<spec>` entries must map, by string name, to the type names | ||
specified in `<types>`, or be an empty array if `<types>` is omitted. The types | ||
in `<spec>` will then be used to map between method implementations for the | ||
individual functions, and the provided types in the impl. | ||
Protocols can include an `opts` object as the last argument, with the following | ||
available options: | ||
* `opts.name` `{String}` - The name to use when referring to the protocol. | ||
* `opts.where` `{Array[Constraint]|Constraint}` - Protocol constraints to use. | ||
* `opts.metaobject` - Accepts an object implementing the | ||
`Protoduck` protocol, which can be used to alter protocol definition | ||
mechanisms in `protoduck`. | ||
##### Example | ||
```javascript | ||
const Eq = protocol(['a', 'b'], { | ||
eq: ['a', 'b'] | ||
const Eq = protoduck.define(['a'], { | ||
eq: ['a'] | ||
}) | ||
``` | ||
#### <a name="impl"></a> `proto(<target>?, <types>?, <implementations>)` | ||
#### <a name="impl"></a> `proto.impl(<target>, <types>?, <implementations>?)` | ||
Adds a new implementation to the given `proto` across `<types>`. | ||
Adds a new implementation to the given protocol across `<types>`. | ||
`<implementations>` must be an object with functions matching the protocol's | ||
API. The types in `<types>` will be used for defining specific methods using | ||
the function as the body. | ||
API. If given, the types in `<types>` will be mapped to their corresponding | ||
method arguments according to the original protocol definition. | ||
Protocol implementations must include either `<target>`, `<types>`, or both: | ||
* If only `<target>` is present, implementations will be defined the same as | ||
"traditional" methods -- that is, the definitions in `<implementations>` | ||
will add function properties directly to `<target>`. | ||
* If only `<types>` is present, the protocol will keep all protocol functions as | ||
"static" methods on the protocol itself. | ||
* If both are specified, protocol implementations will add methods to the `<target>`, and define multimethods using `<types>`. | ||
If a protocol is derivable -- that is, all its functions have default impls, | ||
@@ -331,17 +316,20 @@ then the `<implementations>` object can be omitted entirely, and the protocol | ||
```javascript | ||
import protocol from 'protoduck' | ||
import protoduck from 'protoduck' | ||
// Singly-dispatched protocols | ||
const Show = protocol({ | ||
const Show = protoduck.define({ | ||
show: [] | ||
}) | ||
class Foo {} | ||
class Foo { | ||
constructor (name) { | ||
this.name = name | ||
} | ||
} | ||
Show(Foo, { | ||
Show.impl(Foo, { | ||
show () { return `[object Foo(${this.name})]` } | ||
}) | ||
var f = new Foo() | ||
f.name = 'alex' | ||
const f = new Foo('alex') | ||
f.show() === '[object Foo(alex)]' | ||
@@ -351,6 +339,6 @@ ``` | ||
```javascript | ||
import protocol from 'protoduck' | ||
import protoduck from 'protoduck' | ||
// Multi-dispatched protocols | ||
const Comparable = protocol(['target'], { | ||
const Comparable = protoduck.define(['target'], { | ||
compare: ['target'], | ||
@@ -363,7 +351,7 @@ }) | ||
Comparable(Foo, [Bar], { | ||
Comparable.impl(Foo, [Bar], { | ||
compare (bar) { return 'bars are ok' } | ||
}) | ||
Comparable(Foo, [Baz], { | ||
Comparable.impl(Foo, [Baz], { | ||
compare (baz) { return 'but bazzes are better' } | ||
@@ -370,0 +358,0 @@ }) |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
24246
4
327
7
347
1
+ Addedgenfun@4.0.1(transitive)
- Removedgenfun@3.2.1(transitive)
Updatedgenfun@^4.0.1