• Home
Name
Date
Size
#Lines
LOC

..--

CHANGELOG.mdD12-May-20242.7 KiB6739

LICENSED12-May-20241.1 KiB2217

README.mdD12-May-202410.9 KiB347264

index.jsD12-May-20249.4 KiB350318

package.jsonD12-May-20242.3 KiB8988

README.md

1 # 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)
2 
3 [`protoduck`](https://github.com/zkat/protoduck) is a JavaScript library is a
4 library for making groups of methods, called "protocols".
5 
6 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 ## Install
12 
13 `$ npm install -S protoduck`
14 
15 ## Table of Contents
16 
17 * [Example](#example)
18 * [Features](#features)
19 * [Guide](#guide)
20   * [Introduction](#introduction)
21   * [Defining protocols](#defining-protocols)
22   * [Implementations](#protocol-impls)
23   * [Multiple dispatch](#multiple-dispatch)
24   * [Constraints](#constraints)
25 * [API](#api)
26   * [`define()`](#define)
27   * [`proto.impl()`](#impl)
28 
29 ### Example
30 
31 ```javascript
32 const protoduck = require('protoduck')
33 
34 // Quackable is a protocol that defines three methods
35 const Quackable = protoduck.define({
36   walk: [],
37   talk: [],
38   isADuck: [() => true] // default implementation -- it's optional!
39 })
40 
41 // `duck` must implement `Quackable` for this function to work. It doesn't
42 // matter what type or class duck is, as long as it implements Quackable.
43 function doStuffToDucks (duck) {
44   if (!duck.isADuck()) {
45     throw new Error('I want a duck!')
46   } else {
47     console.log(duck.walk())
48     console.log(duck.talk())
49   }
50 }
51 
52 // ...In a different package:
53 const ducks = require('./ducks')
54 
55 class Duck () {}
56 
57 // Implement the protocol on the Duck class.
58 ducks.Quackable.impl(Duck, {
59   walk () { return "*hobble hobble*" }
60   talk () { return "QUACK QUACK" }
61 })
62 
63 // main.js
64 ducks.doStuffToDucks(new Duck()) // works!
65 ```
66 
67 ### Features
68 
69 * Verifies implementations in case methods are missing or wrong ones added
70 * Helpful, informative error messages
71 * Optional default method implementations
72 * Fresh JavaScript Feelâ„¢ -- methods work just like native methods when called
73 * Methods can dispatch on arguments, not just `this` ([multimethods](https://npm.im/genfun))
74 * Type constraints
75 
76 ### Guide
77 
78 #### Introduction
79 
80 Like most Object-oriented languages, JavaScript comes with its own way of
81 defining methods: You simply add regular `function`s as properties to regular
82 objects, and when you do `obj.method()`, it calls the right code! ES6/ES2015
83 further extended this by adding a `class` syntax that allowed this same system
84 to work with more familiar syntax sugar: `class Foo { method() { ... } }`.
85 
86 The point of "protocols" is to have a more explicit definitions of what methods
87 "go together". That is, a protocol is a description of a type of object your
88 code interacts with. If someone passes an object into your library, and it fits
89 your defined protocol, the assumption is that the object will work just as well.
90 
91 Duck typing is a common term for this sort of thing: If it walks like a duck,
92 and it talks like a duck, then it may as well be a duck, as far as any of our
93 code is concerned.
94 
95 Many other languages have similar or identical concepts under different names:
96 Java's interfaces, Haskell's typeclasses, Rust's traits. Elixir and Clojure both
97 call them "protocols" as well.
98 
99 One big advantage to using these protocols is that they let users define their
100 own versions of some abstraction, without requiring the type to inherit from
101 another -- protocols are independent of inheritance, even though they're able to
102 work together with it. If you've ever found yourself in some sort of inheritance
103 mess, this is exactly the sort of thing you use to escape it.
104 
105 #### Defining Protocols
106 
107 The first step to using `protoduck` is to define a protocol. Protocol
108 definitions look like this:
109 
110 ```javascript
111 // import the library first!
112 const protoduck = require('protoduck')
113 
114 // `Ducklike` is the name of our protocol. It defines what it means for
115 // something to be "like a duck", as far as our code is concerned.
116 const Ducklike = protoduck.define([], {
117   walk: [], // This says that the protocol requires a "walk" method.
118   talk: [] // and ducks also need to talk
119   peck: [] // and they can even be pretty scary
120 })
121 ```
122 
123 Protocols by themselves don't really *do* anything, they simply define what
124 methods are included in the protocol, and thus what will need to be implemented.
125 
126 #### Protocol Impls
127 
128 The simplest type of definitions for protocols are as regular methods. In this
129 style, protocols end up working exactly like normal JavaScript methods: they're
130 added as properties of the target type/object, and we call them using the
131 `foo.method()` syntax. `this` is accessible inside the methods, as usual.
132 
133 Implementation syntax is very similar to protocol definitions, using `.impl`:
134 
135 ```javascript
136 class Dog {}
137 
138 // Implementing `Ducklike` for `Dog`s
139 Ducklike.impl(Dog, [], {
140   walk () { return '*pads on all fours*' }
141   talk () { return 'woof woof. I mean "quack" >_>' }
142   peck (victim) { return 'Can I just bite ' + victim + ' instead?...' }
143 })
144 ```
145 
146 So now, our `Dog` class has two extra methods: `walk`, and `talk`, and we can
147 just call them:
148 
149 ```javascript
150 const pupper = new Dog()
151 
152 pupper.walk() // *pads on all fours*
153 pupper.talk() // woof woof. I mean "quack" >_>
154 pupper.peck('this string') // Can I just bite this string instead?...
155 ```
156 
157 #### Multiple Dispatch
158 
159 You may have noticed before that we have these `[]` in various places that don't
160 seem to have any obvious purpose.
161 
162 These arrays allow protocols to be implemented not just for a single value of
163 `this`, but across *all arguments*. That is, you can have methods in these
164 protocols that use both `this`, and the first argument (or any other arguments)
165 in order to determine what code to actually execute.
166 
167 This type of method is called a multimethod, and is one of the differences
168 between protoduck and the default `class` syntax.
169 
170 To use it: in the protocol *definitions*, you put matching
171 strings in different spots where those empty arrays were, and when you
172 *implement* the protocol, you give the definition the actual types/objects you
173 want to implement it on, and it takes care of mapping types to the strings you
174 defined, and making sure the right code is run:
175 
176 ```javascript
177 const Playful = protoduck.define(['friend'], {// <---\
178   playWith: ['friend'] // <------------ these correspond to each other
179 })
180 
181 class Cat {}
182 class Human {}
183 class Dog {}
184 
185 // The first protocol is for Cat/Human combination
186 Playful.impl(Cat, [Human], {
187   playWith (human) {
188     return '*headbutt* *purr* *cuddle* omg ilu, ' + human.name
189   }
190 })
191 
192 // And we define it *again* for a different combination
193 Playful.impl(Cat, [Dog], {
194   playWith (dog) {
195     return '*scratches* *hisses* omg i h8 u, ' + dog.name
196   }
197 })
198 
199 // depending on what you call it with, it runs different methods:
200 const cat = new Cat()
201 const human = new Human()
202 const dog = new Dog()
203 
204 cat.playWith(human) // *headbutt* *purr* *cuddle* omg ilu, Sam
205 cat.playWith(dog) // *scratches* *hisses* omg i h8 u, Pupper
206 ```
207 
208 #### Constraints
209 
210 Sometimes, you want to have all the functionality of a certain protocol, but you
211 want to add a few requirements or other bits an pieces. Usually, you would have
212 to define the entire functionality of the "parent" protocol in your own protocol
213 in order to pull this off. This isn't very DRY and thus prone to errors, missing
214 or out-of-sync functionality, or other issues. You could also just tell users
215 "hey, if you implement this, make sure to implement that", but there's no
216 guarantee they'll know about it, or know which arguments map to what.
217 
218 This is where constraints come in: You can define a protocol that expects
219 anything that implements it to *also* implement one or more "parent" protocols.
220 
221 ```javascript
222 const Show = proto.define({
223   // This syntax allows default impls without using arrays.
224   toString () {
225     return Object.prototype.toString.call(this)
226   },
227   toJSON () {
228     return JSON.stringify(this)
229   }
230 })
231 
232 const Log = proto.define({
233   log () { console.log(this.toString()) }
234 }, {
235   where: Show()
236   // Also valid:
237   // [Show('this'), Show('a')]
238   // [Show('this', ['a', 'b'])]
239 })
240 
241 // This fails with an error: must implement Show:
242 Log.impl(MyThing)
243 
244 // So derive Show first...
245 Show.impl(MyThing)
246 // And now it's ok!
247 Log.impl(MyThing)
248 ```
249 
250 ### API
251 
252 #### <a name="define"></a> `define(<types>?, <spec>, <opts>)`
253 
254 Defines a new protocol on across arguments of types defined by `<types>`, which
255 will expect implementations for the functions specified in `<spec>`.
256 
257 If `<types>` is missing, it will be treated the same as if it were an empty
258 array.
259 
260 The types in `<spec>` entries must map, by string name, to the type names
261 specified in `<types>`, or be an empty array if `<types>` is omitted. The types
262 in `<spec>` will then be used to map between method implementations for the
263 individual functions, and the provided types in the impl.
264 
265 Protocols can include an `opts` object as the last argument, with the following
266 available options:
267 
268 * `opts.name` `{String}` - The name to use when referring to the protocol.
269 
270 * `opts.where` `{Array[Constraint]|Constraint}` - Protocol constraints to use.
271 
272 * `opts.metaobject` - Accepts an object implementing the
273   `Protoduck` protocol, which can be used to alter protocol definition
274   mechanisms in `protoduck`.
275 
276 ##### Example
277 
278 ```javascript
279 const Eq = protoduck.define(['a'], {
280   eq: ['a']
281 })
282 ```
283 
284 #### <a name="impl"></a> `proto.impl(<target>, <types>?, <implementations>?)`
285 
286 Adds a new implementation to the given protocol across `<types>`.
287 
288 `<implementations>` must be an object with functions matching the protocol's
289 API. If given, the types in `<types>` will be mapped to their corresponding
290 method arguments according to the original protocol definition.
291 
292 If a protocol is derivable -- that is, all its functions have default impls,
293 then the `<implementations>` object can be omitted entirely, and the protocol
294 will be automatically derived for the given `<types>`
295 
296 ##### Example
297 
298 ```javascript
299 import protoduck from 'protoduck'
300 
301 // Singly-dispatched protocols
302 const Show = protoduck.define({
303   show: []
304 })
305 
306 class Foo {
307   constructor (name) {
308     this.name = name
309   }
310 }
311 
312 Show.impl(Foo, {
313   show () { return `[object Foo(${this.name})]` }
314 })
315 
316 const f = new Foo('alex')
317 f.show() === '[object Foo(alex)]'
318 ```
319 
320 ```javascript
321 import protoduck from 'protoduck'
322 
323 // Multi-dispatched protocols
324 const Comparable = protoduck.define(['target'], {
325   compare: ['target'],
326 })
327 
328 class Foo {}
329 class Bar {}
330 class Baz {}
331 
332 Comparable.impl(Foo, [Bar], {
333   compare (bar) { return 'bars are ok' }
334 })
335 
336 Comparable.impl(Foo, [Baz], {
337   compare (baz) { return 'but bazzes are better' }
338 })
339 
340 const foo = new Foo()
341 const bar = new Bar()
342 const baz = new Baz()
343 
344 foo.compare(bar) // 'bars are ok'
345 foo.compare(baz) // 'but bazzes are better'
346 ```
347