├── .gitignore ├── LICENSE ├── README.md ├── dot-compose.js ├── package.json └── test └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Simon Friis Vindum 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dot-compose 2 | 3 | Combines the tersness of chaining with the power and flexibility of function 4 | composition. It temporarily redefines JavaScripts property access operator `.` 5 | into meaning function composition. 6 | 7 | ## Example 8 | 9 | With Ramda. 10 | 11 | ```javascript 12 | var r = dotCompose.Group(R); // Creates a compose group with all functions in `R` 13 | 14 | // ╭── Starts composition 15 | // │ 16 | // │ ╭──────┬────────── The dots represents composition 17 | // ↓ ↓ ↓ 18 | R.map(r.add(2).negate.divide(R.__, 3).$, [3, 6, 9, 12]); //=> [1, 0, -1, -2] 19 | // ↑ ↑ ↑ 20 | // │ │ ╰── The dollar sign ends the composition 21 | // │ │ 22 | // ╰── Partial application works with curried functions 23 | ``` 24 | 25 | ## Tutorial 26 | 27 | ### Composition groups 28 | 29 | A composition group is created with `Group`. 30 | 31 | ```javascript 32 | var c = dotCompose.Group(); 33 | ``` 34 | 35 | Supplying an object adds all functions in the object to the group. 36 | 37 | ```javascript 38 | var r = dotCompose.Group({ 39 | half: function(n) { return n / 2; }, 40 | square: function(n) { return n * n; }, 41 | }); 42 | ``` 43 | 44 | Functions can be added to a group with `add`. 45 | 46 | ```javascript 47 | var group = dotCompose.Group(); 48 | dotCompose.add(group, function add(n, m) { return n + m; }); 49 | ``` 50 | 51 | `add` returns the added function unchanged. 52 | 53 | ```javascript 54 | var group = dotCompose.Group(); 55 | var add = dotCompose.add(group, function add(n, m) { return n + m; }); 56 | ``` 57 | 58 | ### Function injection 59 | 60 | Only functions in the same composition group can be composed together. 61 | Arbitrary functions can be injected and composed with functions from a group by 62 | using `_`. 63 | 64 | ```javascript 65 | var add3 = function(n) { return n + 3; }; 66 | var c = dotCompose.Group({ 67 | half: function(n) { return n / 2; }, 68 | square: function(n) { return n * n; }, 69 | }); 70 | c.half._(add3).square(5); //=> 14 71 | ``` 72 | 73 | ### Currying of composed functions 74 | 75 | If you supply dot-compose a curry function it will automatically curry the 76 | composed functions. 77 | 78 | ```javascript 79 | dotCompose.curryN(R.curryN); 80 | ``` 81 | 82 | ## Environment support 83 | 84 | dot-compose uses getters from ECMAScript 4. It works from Internet Explore 9 85 | and in everything else. 86 | 87 | -------------------------------------------------------------------------------- /dot-compose.js: -------------------------------------------------------------------------------- 1 | var chains = []; 2 | var curryN = function(n, fn) { return fn; }; 3 | 4 | function inject(fn) { 5 | if (this._int !== undefined) { 6 | chains.push([fn]); 7 | return this._int; 8 | } else { 9 | chains[chains.length - 1].push(fn); 10 | return this; 11 | } 12 | } 13 | 14 | var getFnName; 15 | 16 | if ('name' in inject) { 17 | getFnName = function(fn) { return fn.name; }; 18 | } else { 19 | getFnName = function(fn) { 20 | var fnString = fn.toString(); 21 | return fnString.slice('function '.length, fnString.indexOf('(')); 22 | }; 23 | } 24 | 25 | function endComposition() { 26 | var chain = chains.pop(); 27 | var first = chain[chain.length - 1]; 28 | return curryN(first.length, function() { 29 | var i, val = first.apply(undefined, arguments); 30 | for (i = chain.length - 2; i >= 0; --i) { 31 | val = chain[i](val); 32 | } 33 | return val; 34 | }); 35 | } 36 | 37 | function createGroup(obj) { 38 | var group = createComposeable(); 39 | group._int = createComposeable(); // Intermediate composeable 40 | if (obj !== undefined) { 41 | var fn, name; 42 | for (name in obj) { 43 | fn = obj[name]; 44 | if (typeof fn === 'function' && group[name] === undefined) { 45 | add(group, name, fn); 46 | } 47 | } 48 | } 49 | return group; 50 | } 51 | 52 | function createComposeable() { 53 | function fn() { 54 | var chain = chains[chains.length - 1]; 55 | var lastIdx = chain.length - 1; 56 | chain[lastIdx] = chain[lastIdx].apply(null, arguments); 57 | return fn; 58 | } 59 | fn._ = inject; 60 | Object.defineProperty(fn, '$', {get: endComposition}); 61 | return fn; 62 | } 63 | 64 | function add(group, name, fn) { 65 | if (group[name] === undefined) { 66 | Object.defineProperty(group, name, {get: inject.bind(group, fn)}); 67 | Object.defineProperty(group._int, name, {get: inject.bind(group._int, fn)}); 68 | } 69 | return fn; 70 | } 71 | 72 | module.exports = { 73 | Group: createGroup, 74 | add: function(group, fn) { 75 | return add(group, getFnName(fn), fn); 76 | }, 77 | curryN: function(cur) { curryN = cur; } 78 | }; 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dot-compose", 3 | "version": "0.0.1", 4 | "description": "The terseness of chaining with the power of composition.", 5 | "main": "dot-compose.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "mocha" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/paldepind/dot-compose.git" 15 | }, 16 | "keywords": [ 17 | "chaining", 18 | "composition", 19 | "function", 20 | "currying", 21 | "curry" 22 | ], 23 | "author": "Simon Friis Vindum", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/paldepind/dot-compose/issues" 27 | }, 28 | "homepage": "https://github.com/paldepind/dot-compose#readme" 29 | } 30 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var Compose = require('../dot-compose'); 3 | var R = require('ramda'); 4 | 5 | function double(n) { return 2 * n; } 6 | function square(n) { return n * n; } 7 | function half(n) { return n / 2; } 8 | function add2(n) { return n + 2; } 9 | 10 | describe('dot compose', function() { 11 | describe('composition', function() { 12 | var g = Compose.Group(); 13 | var double = Compose.add(g, function double(n) { return 2 * n; }); 14 | var square = Compose.add(g, function square(n) { return n * n; }); 15 | var half = Compose.add(g, function half(n) { return n / 2; }); 16 | var add2 = Compose.add(g, function add2(n) { return n + 2; }); 17 | var add = Compose.add(g, function add(n, m) { 18 | if (arguments.length === 1) { 19 | return function(m) { 20 | return n + m; 21 | }; 22 | } else { 23 | return n + m; 24 | } 25 | }); 26 | var applyTwice = Compose.add(g, function applyTwice(fn, val) { 27 | if (arguments.length === 1) { 28 | return function(val) { 29 | return fn(fn(val)); 30 | }; 31 | } else { 32 | return fn(fn(val)); 33 | } 34 | }); 35 | it('two can compose', function() { 36 | assert.equal(g.double . square .$(3), 18); 37 | }); 38 | it('three can compose', function() { 39 | assert.equal((g.add2 . double . square .$)(3), 20); 40 | }); 41 | it('function can be reused', function() { 42 | var f = g.add2 . double . square .$; 43 | assert.equal(f(9), 164); 44 | assert.equal(f(3), 20); 45 | }); 46 | it('function in chains can be partially applied', function() { 47 | var f = g.double . add(4) . square .$; 48 | assert.equal(f(4), 40); 49 | assert.equal(f(3), 26); 50 | }); 51 | it('first function in chains can be partially applied', function() { 52 | var f = g._(add(2)) . double .$; 53 | assert.equal(f(4), 10); 54 | assert.equal(f(3), 8); 55 | }); 56 | it('first function in chains can be partially applied – better alternative', function() { 57 | var f = g.add(2) . double .$; 58 | assert.equal(f(4), 10); 59 | assert.equal(f(3), 8); 60 | }); 61 | it('is possible to inject arbitrary after second', function() { 62 | var f = g.add2 . half . _(Math.abs) .$; 63 | assert.equal(f(-10), 7); 64 | }); 65 | it('is possible to inject arbitrary as second', function() { 66 | var f = g.add2 . _(Math.abs) . half .$; 67 | assert.equal(f(-10), 7); 68 | }); 69 | it('function can be composed inside composition', function() { 70 | var f = g.add(2) . half . applyTwice(g.half.double.$) .$; 71 | assert.equal(f(4), 4); 72 | }); 73 | }); 74 | describe('integration', function() { 75 | describe('Ramda', function() { 76 | var r = Compose.Group(R); 77 | it('can decorate Ramda', function() { 78 | var fn1 = r.add(2) . inc . negate .$; 79 | var fn2 = R.compose(R.add(2), R.inc, R.negate); 80 | assert.equal(fn1(13), fn2(13)); 81 | }); 82 | it('can compose with Ramda', function() { 83 | Compose.curryN(R.curryN); 84 | var fn = r.negate . add .$; 85 | assert.equal(fn.length, 2); 86 | }); 87 | it('can map function', function() { 88 | assert.deepEqual(R.map(r.add(2).negate.divide(R.__, 3).$, [3, 6, 9, 12]), 89 | [1, 0, -1, -2]); 90 | }); 91 | }); 92 | }); 93 | }); 94 | --------------------------------------------------------------------------------