├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── package.json ├── src └── index.js └── test ├── fixtures └── preset-options │ ├── call-with-a-single-parameter │ ├── actual.js │ ├── expected.js │ └── options.json │ ├── call-with-multiple-parameters │ ├── actual.js │ ├── expected.js │ └── options.json │ ├── call │ ├── actual.js │ ├── expected.js │ └── options.json │ ├── callee-is-property-of-an-object │ ├── actual.js │ ├── expected.js │ └── options.json │ ├── nested-call │ ├── actual.js │ ├── expected.js │ └── options.json │ └── promise-example │ ├── exec.js │ └── options.json └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "transform-es2015-modules-commonjs", 4 | "transform-es2015-parameters", 5 | "transform-es2015-destructuring" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 2 8 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "canonical" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | *.log 5 | .* 6 | !.babelrc 7 | !.editorconfig 8 | !.eslintrc 9 | !.gitignore 10 | !.npmignore 11 | !.travis.yml 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | test 3 | coverage 4 | .* 5 | *.log 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 7 4 | - 6 5 | - 5 6 | script: 7 | - npm run test 8 | notifications: 9 | email: false 10 | sudo: false 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Gajus Kuizinas (http://gajus.com/) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the Gajus Kuizinas (http://gajus.com/) nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL ANUARY BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # babel-plugin-transform-function-composition 2 | 3 | [![Travis build status](http://img.shields.io/travis/gajus/babel-plugin-transform-function-composition/master.svg?style=flat-square)](https://travis-ci.org/gajus/babel-plugin-transform-function-composition) 4 | [![NPM version](http://img.shields.io/npm/v/babel-plugin-transform-function-composition.svg?style=flat-square)](https://www.npmjs.org/package/babel-plugin-transform-function-composition) 5 | [![Canonical Code Style](https://img.shields.io/badge/code%20style-canonical-blue.svg?style=flat-square)](https://github.com/gajus/canonical) 6 | [![Twitter Follow](https://img.shields.io/twitter/follow/kuizinas.svg?style=social&label=Follow)](https://twitter.com/kuizinas) 7 | 8 | Transpiles [function-bind call expressions](https://github.com/babel/babel/tree/master/packages/babel-plugin-syntax-function-bind) to partially applied call expressions. Uses the invocation context to set the last parameter of the callee function. 9 | 10 | Syntactically, the `babel-plugin-syntax-function-bind` also allows the reader to read the functions in left to right order of application, rather than reading from the innermost expression out. 11 | 12 | * Familiar with [Ramda](http://ramdajs.com/)? This transpiler enables a syntactic sugar for [pipe](http://ramdajs.com/docs/#pipe) function. 13 | * Coming from [Clojure](http://clojure.org/)? This transpiler enables a syntactic sugar for [thread-first](http://clojure.org/guides/threading_macros#thread-first) macro. 14 | 15 | > ***BIG WARNING***: This is a proof-of-concept. See [Motivation](#motivation). 16 | 17 | * [Example transpilation](#example-transpilation) 18 | * [Motivation](#motivation) 19 | * [Participating](#participating) 20 | * [ECMAScript Proposal](#ecmascript-proposal) 21 | * [Difference from This-Binding Syntax proposal](#difference-from-this-binding-syntax-proposal) 22 | * [Difference from the Pipeline Operator proposal](#difference-from-the-pipeline-operator-proposal) 23 | * [The reason for using the `::` syntax](#the-reason-for-using-the--syntax) 24 | * [Usage examples](#usage-examples) 25 | * [Implementing Bluebird API](#implementing-bluebird-api) 26 | * [Composing Ramda functions](#composing-ramda-functions) 27 | 28 | ## Example transpilation 29 | 30 | Input: 31 | 32 | ```js 33 | apple 34 | ::foo('foo parameter 0', 'foo parameter 1') 35 | ::bar('bar parameter 0') 36 | ::baz('baz parameter 0'); 37 | ``` 38 | 39 | Output: 40 | 41 | ```js 42 | baz( 43 | 'baz parameter 0', 44 | bar( 45 | 'bar parameter 0', 46 | foo( 47 | 'foo parameter 0', 48 | 'foo parameter 1', 49 | apple 50 | ) 51 | ) 52 | ); 53 | ``` 54 | 55 | ## Motivation 56 | 57 | To make functional programming in JavaScript sweeter 🍧🍨🍦. 58 | 59 | ### Participating 60 | 61 | Help this proposal to get more attention by spreading the word (or [retweet](https://twitter.com/kuizinas/status/798622847102959616) the original announcement)! 62 | 63 | Participate in a [reddit](https://www.reddit.com/r/javascript/comments/5d4u2t/babel_plugin_that_adds_syntactic_sugar/) discussion to share your thoughts and suggestions. 64 | 65 | ## ECMAScript Proposal 66 | 67 | There is no active proposal for this functionality. 68 | 69 | I am looking for feedback. If there is sufficient interest, I will proceed with a proposal. 70 | 71 | ### Difference from This-Binding Syntax proposal 72 | 73 | ECMAScript [This-Binding Syntax](https://github.com/tc39/proposal-bind-operator) proposal introduces a new operator `::` which performs `this` binding and method extraction, i.e. 74 | 75 | The following input: 76 | 77 | ```js 78 | foo::bar()::baz() 79 | ``` 80 | 81 | Becomes the following output: 82 | 83 | ```js 84 | var _context; 85 | 86 | (_context = (_context = foo, bar).call(_context), baz).call(_context); 87 | ``` 88 | 89 | `babel-plugin-transform-function-composition` uses the `::` operator to create a partially applied function such that the left hand side of the operator is set as the the first parameter to the target function on the right hand side, i.e. 90 | 91 | The following input: 92 | 93 | ```js 94 | foo::bar()::baz() 95 | ``` 96 | 97 | Becomes the following output: 98 | 99 | ```js 100 | baz(bar(foo)); 101 | ``` 102 | 103 | ### Difference from the Pipeline Operator proposal 104 | 105 | ECMAScript [Pipeline Operator](https://github.com/mindeavor/es-pipeline-operator) proposal introduces a new operator `|>` which is a syntactic sugar on a function call with a single argument. In other words, `sqrt(64)` is equivalent to `64 |> sqrt`. 106 | 107 | The biggest difference between `::` and `|>` is that the latter permits only a single parameter. 108 | 109 | > NOTE: There is no Babel transpiler for the Pipeline Operator proposal. 110 | > See https://github.com/mindeavor/es-pipeline-operator/issues/33 111 | 112 | ### The reason for using the `::` syntax 113 | 114 | The `::` syntax conflicts with the [This-Binding Syntax](https://github.com/tc39/proposal-bind-operator) proposal. However, at the time of writing this This-Binding Syntax proposal remains in stage 0 without an active champion (see [What's keeping this from Stage 1?](https://github.com/tc39/proposal-bind-operator/issues/24)). In the mean time, the syntax support has been added to Babel ([babel-plugin-syntax-function-bind](https://github.com/babel/babel/tree/master/packages/babel-plugin-syntax-function-bind)). 115 | 116 | The reason for choosing the `::` operator for this proposal is to enable early adoption of this functionality. 117 | 118 | > NOTE: Should this implementation develop into a proposal, it is possible that an alternative syntax will be proposed (e.g. `->>`). 119 | 120 | ## Usage examples 121 | 122 | ### Implementing Bluebird API 123 | 124 | [Bluebird](http://bluebirdjs.com/) is a promise library that provides non-standard utilities used to abstract common `Promise` operations. 125 | 126 | Here is an example of using [`Promise.map`](http://bluebirdjs.com/docs/api/promise.map.html) and [`Promise.filter`](http://bluebirdjs.com/docs/api/promise.filter.html): 127 | 128 | ```js 129 | import Promise from 'bluebird'; 130 | 131 | Promise 132 | .resolve([ 133 | 'foo', 134 | 'bar', 135 | 'baz' 136 | ]) 137 | .map((currentValue) => { 138 | return currentValue.toUpperCase(); 139 | }) 140 | .filter((currentValue) => { 141 | return currentValue.indexOf('B') === 0; 142 | }); 143 | ``` 144 | 145 | Bluebird achieves this by providing a custom implementation of [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) object. This can be achieved by adding `map` and `filter` functions to the native `Promise.prototype`. 146 | 147 | > Note: Augmenting the built-in prototype is considered an [anti-pattern](https://github.com/shichuan/javascript-patterns/blob/master/general-patterns/built-in-prototypes.html). 148 | > This approach is mentioned only for the sake of completeness of the example. 149 | 150 | You can use `::` to achieve an equivalent composition: 151 | 152 | ```js 153 | const map = async (callback, promise) => { 154 | const values = await promise; 155 | 156 | return values.map(callback); 157 | }; 158 | 159 | const filter = async (callback, promise) => { 160 | const values = await promise; 161 | 162 | return values.filter(callback); 163 | }; 164 | 165 | Promise 166 | .resolve([ 167 | 'foo', 168 | 'bar', 169 | 'baz' 170 | ]) 171 | ::map((currentValue) => { 172 | return currentValue.toUpperCase(); 173 | }) 174 | ::filter((currentValue) => { 175 | return currentValue.indexOf('B') === 0; 176 | }); 177 | ``` 178 | 179 | In both cases, the result of the operation is: 180 | 181 | ```js 182 | [ 183 | 'BAR', 184 | 'BAZ' 185 | ] 186 | ``` 187 | 188 | Bluebird is heavy dependency (31Kb). Using function composition you have implemented equivalent functionality without the bundle size overhead. 189 | 190 | ### Composing Ramda functions 191 | 192 | [Ramda](https://github.com/ramda/ramda) is a functional flavor utility library. Ramda is designed to enable build functions as sequences of simpler functions, each of which transforms the data and passes it along to the next. 193 | 194 | Here is an example of using [`R.pipe`](http://ramdajs.com/docs/#pipe) to perform left-to-right function composition: 195 | 196 | ```js 197 | import { 198 | assocPath, 199 | pipe 200 | } from 'ramda'; 201 | 202 | pipe( 203 | assocPath(['repository', 'type'], 'git'), 204 | assocPath(['repository', 'url'], 'https://github.com/gajus/babel-plugin-transform-function-composition') 205 | )({ 206 | name: 'babel-plugin-transform-function-composition' 207 | }) 208 | ``` 209 | 210 | You can use `::` to achieve an equivalent composition: 211 | 212 | ```js 213 | import { 214 | assocPath 215 | } from 'ramda'; 216 | 217 | ({ 218 | name: 'babel-plugin-transform-function-composition' 219 | }) 220 | ::assocPath(['repository', 'type'], 'git') 221 | ::assocPath(['repository', 'url'], 'https://github.com/gajus/babel-plugin-transform-function-composition'); 222 | 223 | ``` 224 | 225 | In both cases, the result of the operation is: 226 | 227 | ```js 228 | { 229 | name: 'babel-plugin-transform-function-composition', 230 | repository: { 231 | type: 'git', 232 | url: 'https://github.com/gajus/babel-plugin-transform-function-composition' 233 | } 234 | } 235 | ``` 236 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "email": "gajus@gajus.com", 4 | "name": "Gajus Kuizinas", 5 | "url": "http://gajus.com" 6 | }, 7 | "dependencies": { 8 | "babel-plugin-syntax-function-bind": "^6.8.0" 9 | }, 10 | "description": "Syntactic sugar for easy to read function composition.", 11 | "devDependencies": { 12 | "babel-cli": "^6.18.0", 13 | "babel-helper-plugin-test-runner": "^6.18.0", 14 | "babel-plugin-transform-async-to-generator": "^6.16.0", 15 | "babel-plugin-transform-es2015-destructuring": "^6.18.0", 16 | "babel-plugin-transform-es2015-modules-commonjs": "^6.18.0", 17 | "babel-plugin-transform-es2015-parameters": "^6.18.0", 18 | "eslint": "^3.10.1", 19 | "eslint-config-canonical": "^5.5.0", 20 | "husky": "^0.11.9", 21 | "mocha": "^3.1.2" 22 | }, 23 | "engines": { 24 | "node": ">5.0.0" 25 | }, 26 | "keywords": [ 27 | "babel-plugin" 28 | ], 29 | "license": "BSD-3-Clause", 30 | "main": "dist/index.js", 31 | "name": "babel-plugin-transform-function-composition", 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/gajus/babel-plugin-transform-function-composition" 35 | }, 36 | "scripts": { 37 | "build": "NODE_ENV=production babel ./src --out-dir ./dist", 38 | "lint": "eslint ./src", 39 | "precommit": "npm run test", 40 | "test": "NODE_ENV=development npm run lint && npm run build && mocha --compilers js:babel-register" 41 | }, 42 | "version": "1.0.0" 43 | } 44 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export default function ({ 2 | types: t 3 | }) { 4 | return { 5 | 6 | // eslint-disable-next-line global-require 7 | inherits: require('babel-plugin-syntax-function-bind'), 8 | visitor: { 9 | CallExpression ({ 10 | node 11 | }) { 12 | const bind = node.callee; 13 | 14 | if (!t.isBindExpression(bind)) { 15 | return; 16 | } 17 | 18 | const context = bind.object; 19 | 20 | node.callee = bind.callee; 21 | 22 | node.arguments.push(context); 23 | } 24 | } 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /test/fixtures/preset-options/call-with-a-single-parameter/actual.js: -------------------------------------------------------------------------------- 1 | Promise::foo('foo0'); 2 | -------------------------------------------------------------------------------- /test/fixtures/preset-options/call-with-a-single-parameter/expected.js: -------------------------------------------------------------------------------- 1 | foo('foo0', Promise); 2 | -------------------------------------------------------------------------------- /test/fixtures/preset-options/call-with-a-single-parameter/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "../../../../dist" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/preset-options/call-with-multiple-parameters/actual.js: -------------------------------------------------------------------------------- 1 | Promise::foo('foo0', 'foo1', 'foo2'); 2 | -------------------------------------------------------------------------------- /test/fixtures/preset-options/call-with-multiple-parameters/expected.js: -------------------------------------------------------------------------------- 1 | foo('foo0', 'foo1', 'foo2', Promise); 2 | -------------------------------------------------------------------------------- /test/fixtures/preset-options/call-with-multiple-parameters/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "../../../../dist" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/preset-options/call/actual.js: -------------------------------------------------------------------------------- 1 | Promise::foo(); 2 | -------------------------------------------------------------------------------- /test/fixtures/preset-options/call/expected.js: -------------------------------------------------------------------------------- 1 | foo(Promise); 2 | -------------------------------------------------------------------------------- /test/fixtures/preset-options/call/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "../../../../dist" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/preset-options/callee-is-property-of-an-object/actual.js: -------------------------------------------------------------------------------- 1 | Promise::foo0.foo1()::bar0.bar1(); 2 | -------------------------------------------------------------------------------- /test/fixtures/preset-options/callee-is-property-of-an-object/expected.js: -------------------------------------------------------------------------------- 1 | bar0.bar1(foo0.foo1(Promise)); 2 | -------------------------------------------------------------------------------- /test/fixtures/preset-options/callee-is-property-of-an-object/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "../../../../dist" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/preset-options/nested-call/actual.js: -------------------------------------------------------------------------------- 1 | Promise::foo('foo0')::bar('bar0'); 2 | -------------------------------------------------------------------------------- /test/fixtures/preset-options/nested-call/expected.js: -------------------------------------------------------------------------------- 1 | bar('bar0', foo('foo0', Promise)); 2 | -------------------------------------------------------------------------------- /test/fixtures/preset-options/nested-call/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "../../../../dist" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/preset-options/promise-example/exec.js: -------------------------------------------------------------------------------- 1 | const map = async (callback, promise) => { 2 | const values = await promise; 3 | 4 | return values.map(callback); 5 | }; 6 | 7 | const filter = async (callback, promise) => { 8 | const values = await promise; 9 | 10 | return values.filter(callback); 11 | }; 12 | 13 | return Promise 14 | .resolve([ 15 | 'foo', 16 | 'bar', 17 | 'baz' 18 | ]) 19 | ::map((currentValue) => { 20 | return currentValue.toUpperCase(); 21 | }) 22 | ::filter((currentValue) => { 23 | return currentValue.indexOf('B') === 0; 24 | }) 25 | .then((values) => { 26 | assert.deepEqual(values, [ 27 | 'BAR', 28 | 'BAZ' 29 | ]); 30 | }); 31 | -------------------------------------------------------------------------------- /test/fixtures/preset-options/promise-example/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOpts": { 3 | "allowReturnOutsideFunction": true 4 | }, 5 | "plugins": [ 6 | "transform-async-to-generator", 7 | "../../../../dist" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | require('babel-helper-plugin-test-runner')(__dirname); 2 | --------------------------------------------------------------------------------