├── .npmignore ├── package.json ├── examples ├── async.js └── sync.js ├── src ├── utility.js └── jducers │ ├── sync.js │ └── async.js ├── LICENSE └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | examples/* 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jducers", 3 | "version": "0.1.201", 4 | "description": "A js transducers-like implementation using generators", 5 | "main": "index.js", 6 | "directories": { 7 | "example": "examples" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/jfet97/jducers.git" 15 | }, 16 | "keywords": [ 17 | "javascript", 18 | "generators" 19 | ], 20 | "author": "Andrea Simone Costa ", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/jfet97/jducers/issues" 24 | }, 25 | "homepage": "https://github.com/jfet97/jducers#readme" 26 | } 27 | -------------------------------------------------------------------------------- /examples/async.js: -------------------------------------------------------------------------------- 1 | import { pipe, compose } from 'jducers/src/utility'; 2 | import * as AJ from 'jducers/src/jducers/async' 3 | 4 | const asyncArray = { 5 | array: [1, 2, 3, 4, 5], 6 | [Symbol.asyncIterator]: async function* () { 7 | for (const x of this.array) { 8 | await new Promise(ok => setTimeout(() => ok(x), 3000)); 9 | yield x; 10 | } 11 | } 12 | } 13 | 14 | const sumReducer = AJ.reduce((x, y) => x + y, 0); 15 | const doubleMap = AJ.map(x => x * 2); 16 | 17 | (async function IIFE() { 18 | console.log(await AJ.run(pipe(doubleMap, sumReducer), asyncArray)); 19 | })(); 20 | 21 | 22 | let myObserver = AJ.observerFactory(console.log, (x) => console.log(x + x)); 23 | 24 | (async function IIFE() { 25 | await AJ.run(pipe(myObserver, doubleMap, myObserver, sumReducer, myObserver), asyncArray); 26 | })(); 27 | -------------------------------------------------------------------------------- /src/utility.js: -------------------------------------------------------------------------------- 1 | const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x); 2 | 3 | const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x); 4 | 5 | const curryForReduce = (fn) => (par1, par2) => (par3) => fn(par1, par2, par3); 6 | 7 | const curry = fn => { 8 | let arity = fn.length; 9 | return (...args) => { 10 | let firstArgs = args.length; 11 | if (firstArgs >= arity) { 12 | return fn(...args); 13 | } else { 14 | return (...secondArgs) => { 15 | return fn(...[...args, ...secondArgs]); 16 | } 17 | } 18 | } 19 | } 20 | 21 | const partial = (fn, ...preset) => (...later) => fn(...preset, ...later); 22 | const partialRight = (fn, ...preset) => (...later) => fn(...later, ...preset); 23 | 24 | export { pipe, compose, curryForReduce, curry, partial, partialRight } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Andrea Simone Costa 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 | -------------------------------------------------------------------------------- /examples/sync.js: -------------------------------------------------------------------------------- 1 | import { pipe, compose } from 'jducers/src/utility'; 2 | import * as SJ from 'jducers/src/jducers/sync' 3 | 4 | let array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]; 5 | 6 | const isOdd = i => !!(i % 2); // predicate 7 | const double = i => i * 2; // map cb 8 | const minusOne = i => --i; // map cb 9 | const plusTre = (acc, val) => (acc.push(val + 3), acc); // reducer 10 | const sum = (acc, val) => acc + val; // reducer 11 | const mult = (acc, val) => acc * val; // reducer 12 | 13 | const doubleMap = SJ.map(double); 14 | const minusOneMap = SJ.map(minusOne); 15 | const isOddFilter = SJ.filter(isOdd); 16 | const plusThreeReduce = SJ.reduce(plusTre, []); 17 | const sumReduce = SJ.reduce(sum); 18 | const multiplyReduce = SJ.reduce(mult, 1); 19 | 20 | 21 | let jducer = pipe(isOddFilter, minusOneMap, doubleMap, plusThreeReduce); 22 | let res = SJ.run(jducer, array); 23 | console.log(res); 24 | 25 | let jducer2 = compose(doubleMap, minusOneMap, isOddFilter); 26 | res = SJ.run(jducer2, array); 27 | console.log(res); 28 | 29 | let jducer3 = compose(sumReduce, doubleMap, minusOneMap, isOddFilter); 30 | res = SJ.run(jducer3, array); 31 | console.log(res); 32 | 33 | let jducer4 = compose(multiplyReduce, doubleMap, isOddFilter); 34 | res = SJ.run(jducer4, array); 35 | console.log(res); 36 | 37 | let jducer5 = compose(doubleMap, sumReduce, minusOneMap, isOddFilter); 38 | res = SJ.run(jducer5, array); 39 | console.log(res); 40 | -------------------------------------------------------------------------------- /src/jducers/sync.js: -------------------------------------------------------------------------------- 1 | import { curryForReduce, curry } from './../utility'; 2 | 3 | function* _map(fun, input) { 4 | for (const x of input) yield fun(x); 5 | } 6 | 7 | function* _filter(pred, input) { 8 | for (const x of input) pred(x) ? yield x : null; 9 | } 10 | 11 | function* _reduce(reducer, initValue, input) { 12 | 13 | // if input was already an iterator, following operation return itself 14 | // if input was not an iterator, following operation return its iterator 15 | const iterator = input[Symbol.iterator](); 16 | 17 | let flag = false; 18 | if (initValue instanceof Array) flag = true; 19 | 20 | // if the output is an array, reduce has to behave like a map/filter 21 | // values must flow 22 | // if the output is a single number for example, reduce has to interrupt 23 | // the flows of values until she has done her job 24 | 25 | let acc; 26 | if (initValue != undefined) { 27 | acc = initValue; 28 | } else { 29 | acc = iterator.next().value; 30 | if (flag) yield acc; 31 | } 32 | 33 | for (const x of iterator) { 34 | acc = reducer(acc, x); 35 | if (flag) yield acc[acc.length ? acc.length - 1 : 0] 36 | } 37 | if (!flag) yield acc; 38 | } 39 | 40 | function run(jducer, input) { 41 | const res = []; 42 | for (const x of jducer(input)) { 43 | res.push(x); 44 | } 45 | return res.length == 1 ? res[0] : res; 46 | } 47 | 48 | const map = curry(_map); 49 | const filter = curry(_filter); 50 | const reduce = curryForReduce(_reduce); 51 | 52 | export { map, filter, reduce, run } 53 | -------------------------------------------------------------------------------- /src/jducers/async.js: -------------------------------------------------------------------------------- 1 | import { curryForReduce, curry } from './../utility'; 2 | 3 | 4 | 5 | async function* _filter(pred, input) { 6 | for await (const x of input) pred(x) ? yield x : null; 7 | } 8 | 9 | async function* _map(fun, input) { 10 | for await (const x of input) yield fun(x); 11 | } 12 | 13 | async function* _reduce(reducer, initValue, input) { 14 | 15 | const iterator = input[Symbol.asyncIterator](); 16 | 17 | let flag = false; 18 | if (initValue instanceof Array) flag = true; 19 | 20 | let acc; 21 | if (initValue != undefined) { 22 | acc = initValue; 23 | } else { 24 | acc = (await iterator.next()).value; 25 | if (flag) yield acc; 26 | } 27 | 28 | for await (const x of iterator) { 29 | acc = reducer(acc, x); 30 | if (flag) yield acc[acc.length ? acc.length - 1 : 0] 31 | } 32 | if (!flag) yield acc; 33 | } 34 | 35 | function observerFactory(...cbs) { 36 | 37 | const callbacks = [...cbs]; 38 | 39 | async function* observer(input) { 40 | for await (const x of input) { 41 | callbacks.forEach(cb => cb(x)); 42 | yield x; 43 | } 44 | } 45 | 46 | observer.add = (...cbs) => observer.callbacks.push(...cbs); 47 | return observer; 48 | } 49 | 50 | 51 | 52 | async function run(jducer, input) { 53 | const res = []; 54 | for await (const x of jducer(input)) { 55 | res.push(x); 56 | } 57 | return res.length == 1 ? res[0] : res; 58 | } 59 | 60 | 61 | 62 | const map = curry(_map); 63 | const filter = curry(_filter); 64 | const reduce = curryForReduce(_reduce); 65 | 66 | export { map, filter, reduce, run, observerFactory } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jducers 2 | A js transducers-like implementation using generators and ES9 async generators. 3 | 4 | A composed function expecting a combination function to make a reducer is called **transducer**. 5 | Transducers are useful to compose adjacent **map()**, **filer()** and **reduce()** operations together to improve performaces. 6 | 7 | A **jducer** is similar to a transducer because it is a composed function and can be used to compose **map()**, **filter()** and **reduce()** operations together, but there are some differences. 8 | 9 | It was a challenge against myself to see how far could I go using generators. 10 | 11 | ## install 12 | 13 | ```sh 14 | $ npm i --save jducers 15 | ``` 16 | 17 | ## utility 18 | A little fp utility library used by jducers and available for you with **pipe**, **compose**, **curry**, **partial** and **partialRight** 19 | 20 | ```js 21 | import { pipe, compose, curry, partial, partialRight } from 'jducers/src/utility'; 22 | ``` 23 | 24 | ## sync 25 | Sync's jducers helpers: 26 | 27 | ```js 28 | import { map, filter, reduce, run } from 'jducers/src/jducers/sync' 29 | ``` 30 | or 31 | ```js 32 | import * as SJ from 'jducers/src/jducers/sync' 33 | ``` 34 | 35 | * **map**: accepts only a mapper function that is up to you and returns a function used in the creation of a **jducer** 36 | * **filter**: accepts only a predicate function that is up to you and returns a function used in the creation of a **jducer** 37 | * **reduce**: accepts two parameters: a reducer function with some constraints and an optional initial value and returns a function used in the creation of a **jducer**. The reducer function will be called with only two parameters: the accumulator and the current processed value 38 | * **run**: accepts two parameters: a composition (you can use **pipe** or **compose** from my utility library) and a sync iterable like an array. Returns the resuling array or a single value.It depends on the functions used in the composition 39 | 40 | ```js 41 | import * as SJ from 'jducers/src/jducers/sync' 42 | import { pipe, partialRight } from 'jducers/src/utility'; 43 | 44 | let array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]; 45 | 46 | const isOdd = i => !!(i % 2); // predicate function 47 | const double = i => i * 2; // mapper function 48 | const sum = (acc, val) => acc + val; // reducer 49 | 50 | const syncIsOddFilter = SJ.filter(isOdd); 51 | const syncDoubleMap = SJ.map(double); 52 | const syncSumReduce = SJ.reduce(sum); 53 | 54 | const run = partialRight(SJ.run, array); 55 | 56 | let jducer = pipe(syncIsOddFilter, syncDoubleMap); 57 | let res = run(jducer); 58 | console.log(res); // [2, 6, 10, 14, 18, 22, 26, 30, 34, 38] 59 | 60 | jducer = pipe(syncIsOddFilter, syncDoubleMap, syncSumReduce); 61 | res = run(jducer); 62 | console.log(res); // 200 63 | ``` 64 | 65 | ## async 66 | Async's jducers helpers (useful for async iterables and concurrent async iterations): 67 | 68 | ```js 69 | import { map, filter, reduce, run, observerFactory } from 'jducers/src/jducers/async' 70 | ``` 71 | or 72 | ```js 73 | import * as AJ from 'jducers/src/jducers/async' 74 | ``` 75 | 76 | * **map**: accepts only a mapper function that is up to you and returns a function used in the creation of a **jducer** 77 | * **filter**: accepts only a predicate function that is up to you and returns a function used in the creation of a **jducer** 78 | * **reduce**: accepts two parameters: a reducer function with some constraints and an optional initial value and returns a function used in the creation of a **jducer**. The reducer function will be called with only two parameters: the accumulator and the current processed value 79 | * **run**: accepts two parameters: a composition (you can use **pipe** or **compose** from my utility library) and an async iterable like an array of promises. Returns a promise that will be fulfilled with the resulting array or a single value. It depends on the functions used in the composition 80 | * **observerFactory**: accepts one or more callbacks and returns a simple observer that calls them when each single value of our async iterable flows through it. The observer has to be placed in a composition to form the **jducer** 81 | 82 | ```js 83 | import * as AJ from 'jducers/src/jducers/async' 84 | import { pipe, partialRight } from 'jducers/src/utility'; 85 | 86 | const asyncArray = { 87 | array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 88 | [Symbol.asyncIterator]: async function* () { 89 | for (const x of this.array) { 90 | await new Promise(ok => setTimeout(() => ok(x), 2000)); 91 | yield x; 92 | } 93 | } 94 | } 95 | 96 | const isOdd = i => !!(i % 2); // predicate function 97 | const double = i => i * 2; // mapper function 98 | const sum = (acc, val) => acc + val; // reducer 99 | 100 | const asyncIsOddFilter = AJ.filter(isOdd); 101 | const asyncDoubleMap = AJ.map(double); 102 | const asyncSumReduce = AJ.reduce(sum); 103 | 104 | const run = partialRight(AJ.run, asyncArray); 105 | 106 | let jducer = pipe(asyncIsOddFilter, asyncDoubleMap); 107 | let res = run(jducer); 108 | res.then(x => console.log(x)); // [2, 6, 10, 14, 18, 22, 26, 30, 34, 38] 109 | 110 | jducer = pipe(asyncIsOddFilter, asyncDoubleMap, asyncSumReduce); 111 | res = run(jducer); 112 | res.then(x => console.log(x)); // 200 113 | 114 | const observer = AJ.observerFactory(console.log); 115 | /* 116 | const observer = observerFactory(); 117 | observer.add(console.log); 118 | */ 119 | 120 | jducer = pipe(observer, asyncDoubleMap, observer, asyncSumReduce); 121 | // we will see each value before and after the double mapper function 122 | // 1 2 2 4 3 6 4 8 5 10 6 12 ... 123 | res = run(jducer); 124 | res.then(x => console.log(x)); // 420 125 | 126 | // WARNING: OUTPUTS ARE IN CONCURRENCY 127 | ``` 128 | 129 | ## weight 130 | 131 | All modules and functions together are 3.284Kb **without** compression 132 | 133 | ## performance 134 | 135 | Bad, because **yield** is still an expensive operation 136 | 137 | ## fun 138 | 139 | A lot! ES9 is so powerful, in few lines I created something very difficult to do before without a library 140 | 141 | ## license 142 | 143 | MIT 144 | 145 | --------------------------------------------------------------------------------