Name |
Date |
Size |
#Lines |
LOC |
||
---|---|---|---|---|---|---|
.. | - | - | ||||
CHANGELOG.md | D | 12-May-2024 | 2.7 KiB | 67 | 39 | |
LICENSE | D | 12-May-2024 | 1.1 KiB | 22 | 17 | |
README.md | D | 12-May-2024 | 10.9 KiB | 347 | 264 | |
index.js | D | 12-May-2024 | 9.4 KiB | 350 | 318 | |
package.json | D | 12-May-2024 | 2.3 KiB | 89 | 88 |
README.md
1# protoduck [](https://npm.im/protoduck) [](https://npm.im/protoduck) [](https://travis-ci.org/zkat/protoduck) [](https://ci.appveyor.com/project/zkat/protoduck) [](https://coveralls.io/github/zkat/protoduck?branch=latest) 2 3[`protoduck`](https://github.com/zkat/protoduck) is a JavaScript library is a 4library for making groups of methods, called "protocols". 5 6If you're familiar with the concept of ["duck 7typing"](https://en.wikipedia.org/wiki/Duck_typing), then it might make sense to 8think of protocols as things that explicitly define what methods you need in 9order 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 32const protoduck = require('protoduck') 33 34// Quackable is a protocol that defines three methods 35const 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. 43function 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: 53const ducks = require('./ducks') 54 55class Duck () {} 56 57// Implement the protocol on the Duck class. 58ducks.Quackable.impl(Duck, { 59 walk () { return "*hobble hobble*" } 60 talk () { return "QUACK QUACK" } 61}) 62 63// main.js 64ducks.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 80Like most Object-oriented languages, JavaScript comes with its own way of 81defining methods: You simply add regular `function`s as properties to regular 82objects, and when you do `obj.method()`, it calls the right code! ES6/ES2015 83further extended this by adding a `class` syntax that allowed this same system 84to work with more familiar syntax sugar: `class Foo { method() { ... } }`. 85 86The 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 88code interacts with. If someone passes an object into your library, and it fits 89your defined protocol, the assumption is that the object will work just as well. 90 91Duck typing is a common term for this sort of thing: If it walks like a duck, 92and it talks like a duck, then it may as well be a duck, as far as any of our 93code is concerned. 94 95Many other languages have similar or identical concepts under different names: 96Java's interfaces, Haskell's typeclasses, Rust's traits. Elixir and Clojure both 97call them "protocols" as well. 98 99One big advantage to using these protocols is that they let users define their 100own versions of some abstraction, without requiring the type to inherit from 101another -- protocols are independent of inheritance, even though they're able to 102work together with it. If you've ever found yourself in some sort of inheritance 103mess, this is exactly the sort of thing you use to escape it. 104 105#### Defining Protocols 106 107The first step to using `protoduck` is to define a protocol. Protocol 108definitions look like this: 109 110```javascript 111// import the library first! 112const 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. 116const 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 123Protocols by themselves don't really *do* anything, they simply define what 124methods are included in the protocol, and thus what will need to be implemented. 125 126#### Protocol Impls 127 128The simplest type of definitions for protocols are as regular methods. In this 129style, protocols end up working exactly like normal JavaScript methods: they're 130added 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 133Implementation syntax is very similar to protocol definitions, using `.impl`: 134 135```javascript 136class Dog {} 137 138// Implementing `Ducklike` for `Dog`s 139Ducklike.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 146So now, our `Dog` class has two extra methods: `walk`, and `talk`, and we can 147just call them: 148 149```javascript 150const pupper = new Dog() 151 152pupper.walk() // *pads on all fours* 153pupper.talk() // woof woof. I mean "quack" >_> 154pupper.peck('this string') // Can I just bite this string instead?... 155``` 156 157#### Multiple Dispatch 158 159You may have noticed before that we have these `[]` in various places that don't 160seem to have any obvious purpose. 161 162These 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 164protocols that use both `this`, and the first argument (or any other arguments) 165in order to determine what code to actually execute. 166 167This type of method is called a multimethod, and is one of the differences 168between protoduck and the default `class` syntax. 169 170To use it: in the protocol *definitions*, you put matching 171strings in different spots where those empty arrays were, and when you 172*implement* the protocol, you give the definition the actual types/objects you 173want to implement it on, and it takes care of mapping types to the strings you 174defined, and making sure the right code is run: 175 176```javascript 177const Playful = protoduck.define(['friend'], {// <---\ 178 playWith: ['friend'] // <------------ these correspond to each other 179}) 180 181class Cat {} 182class Human {} 183class Dog {} 184 185// The first protocol is for Cat/Human combination 186Playful.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 193Playful.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: 200const cat = new Cat() 201const human = new Human() 202const dog = new Dog() 203 204cat.playWith(human) // *headbutt* *purr* *cuddle* omg ilu, Sam 205cat.playWith(dog) // *scratches* *hisses* omg i h8 u, Pupper 206``` 207 208#### Constraints 209 210Sometimes, you want to have all the functionality of a certain protocol, but you 211want to add a few requirements or other bits an pieces. Usually, you would have 212to define the entire functionality of the "parent" protocol in your own protocol 213in order to pull this off. This isn't very DRY and thus prone to errors, missing 214or 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 216guarantee they'll know about it, or know which arguments map to what. 217 218This is where constraints come in: You can define a protocol that expects 219anything that implements it to *also* implement one or more "parent" protocols. 220 221```javascript 222const 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 232const 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: 242Log.impl(MyThing) 243 244// So derive Show first... 245Show.impl(MyThing) 246// And now it's ok! 247Log.impl(MyThing) 248``` 249 250### API 251 252#### <a name="define"></a> `define(<types>?, <spec>, <opts>)` 253 254Defines a new protocol on across arguments of types defined by `<types>`, which 255will expect implementations for the functions specified in `<spec>`. 256 257If `<types>` is missing, it will be treated the same as if it were an empty 258array. 259 260The types in `<spec>` entries must map, by string name, to the type names 261specified in `<types>`, or be an empty array if `<types>` is omitted. The types 262in `<spec>` will then be used to map between method implementations for the 263individual functions, and the provided types in the impl. 264 265Protocols can include an `opts` object as the last argument, with the following 266available 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 279const Eq = protoduck.define(['a'], { 280 eq: ['a'] 281}) 282``` 283 284#### <a name="impl"></a> `proto.impl(<target>, <types>?, <implementations>?)` 285 286Adds a new implementation to the given protocol across `<types>`. 287 288`<implementations>` must be an object with functions matching the protocol's 289API. If given, the types in `<types>` will be mapped to their corresponding 290method arguments according to the original protocol definition. 291 292If a protocol is derivable -- that is, all its functions have default impls, 293then the `<implementations>` object can be omitted entirely, and the protocol 294will be automatically derived for the given `<types>` 295 296##### Example 297 298```javascript 299import protoduck from 'protoduck' 300 301// Singly-dispatched protocols 302const Show = protoduck.define({ 303 show: [] 304}) 305 306class Foo { 307 constructor (name) { 308 this.name = name 309 } 310} 311 312Show.impl(Foo, { 313 show () { return `[object Foo(${this.name})]` } 314}) 315 316const f = new Foo('alex') 317f.show() === '[object Foo(alex)]' 318``` 319 320```javascript 321import protoduck from 'protoduck' 322 323// Multi-dispatched protocols 324const Comparable = protoduck.define(['target'], { 325 compare: ['target'], 326}) 327 328class Foo {} 329class Bar {} 330class Baz {} 331 332Comparable.impl(Foo, [Bar], { 333 compare (bar) { return 'bars are ok' } 334}) 335 336Comparable.impl(Foo, [Baz], { 337 compare (baz) { return 'but bazzes are better' } 338}) 339 340const foo = new Foo() 341const bar = new Bar() 342const baz = new Baz() 343 344foo.compare(bar) // 'bars are ok' 345foo.compare(baz) // 'but bazzes are better' 346``` 347