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 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