From f6289b0d7f4a649ed27c8780f63c7f8fa79eefee Mon Sep 17 00:00:00 2001 From: Declan de Wet Date: Tue, 14 Jun 2016 23:48:47 +0200 Subject: [PATCH] add functional auto-curried syntax support --- README.md | 56 +++++++++++++++++++++++++++++--- index.js | 50 +++++++++++++++++++++++----- test/curried.js | 31 ++++++++++++++++++ test/functional.js | 27 +++++++++++++++ test/{index.js => imperative.js} | 0 5 files changed, 151 insertions(+), 13 deletions(-) create mode 100644 test/curried.js create mode 100644 test/functional.js rename test/{index.js => imperative.js} (100%) diff --git a/README.md b/README.md index ea9fdde..9478158 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![npm](http://img.shields.io/npm/v/objectfn.svg?style=flat)](https://badge.fury.io/js/objectfn) [![tests](http://img.shields.io/travis/jescalan/objectfn/master.svg?style=flat)](https://travis-ci.org/jescalan/objectfn) [![dependencies](http://img.shields.io/david/jescalan/objectfn.svg?style=flat)](https://david-dm.org/jescalan/objectfn) [![coverage](http://img.shields.io/coveralls/jescalan/objectfn.svg?style=flat)](https://coveralls.io/github/jescalan/objectfn) -Map, reduce, forEach, and filter for objects. Lazy evaluation, no dependencies. +`map`, `reduce`, `forEach`, and `filter` for plain objects. Lazy evaluation, supports functional & imperative syntax, no dependencies. ### Why should you care? @@ -10,15 +10,23 @@ I wanted a library that has no dependencies and gives me the basic map/reduce/fi Also, big props to [@declandewet](https://github.com/declandewet) for the initial implementation of this library! +### Requirements + +- [Node.js v.6+](http://nodejs.org) + ### Installation -`npm install objectfn -S` +Using a terminal: -> **Note:** This project is compatible with node v6+ only +`npm install objectfn -S` ### Usage -Very straightforward usage. Just import what you need and use it on an object. +Usage is straightforward. Just import what you need and use it on an object. + +#### Imperative style + +Takes data first, callback last. ```js const {map, reduce, filter, forEach} = require('objectfn') @@ -38,9 +46,39 @@ forEach(obj, console.log.bind(console)) // logs out all the values ``` +#### Functional style + +Takes callback first, data last. Each method is automatically curried. + +```js +const {map, reduce, filter, forEach} = require('objectfn') + +const obj = { foo: 'bar', wow: 'doge' } + +const mapper = map((val, key) => val.toUpperCase()) +mapper(obj) +// { foo: 'BAR', wow: 'DOGE' } + +const reducer = reduce((accum, val, key) => accum[val.toUpperCase()] = key && accum }, {}) +reducer(obj) +// { FOO: 'bar', WOW: 'doge' } + +const filterer = filter((val, key) => !key === 'foo') +filterer(obj) +// { wow: 'doge' } + +const iterator = forEach(console.log.bind(console)) +iterator(obj) +// logs out all the values +``` + +### Method Signature + Each callback has a method signature of `(value, key, index, object)` with the exception of `reduce`, which has `(accumulator, value, key, index, object)`. `value` is the current key's value, `key` is the current key's name, `index` is the 0-based index of the current key and `object` is the original object. -**Note:** Unlike the native array equivalent as well as other library implementations, we felt it would be better to explicitly require the passing of an accumulator to the `reduce` method. +### Differences in `reduce` + +Unlike the native array equivalent as well as other library implementations, we felt it would be better to explicitly require the passing of an accumulator to the `reduce` method. This means that this will work: @@ -56,6 +94,14 @@ let obj = { one: 1, two: 2, three: 3, four: 4 } reduce(obj, (prevVal, currVal) => prevVal + currVal) // => wat? ``` +### Binding `this` + +Unlike native (and a small handful of other third-party) implementations, this library offers no mechanism for binding the `this` context of the callback via the last parameter. If you want this, it is far more readable to do so using `Function.prototype.bind`: + +```js +map(obj, fn.bind(/* value to use as `this` goes here */)) +``` + ### License & Contributing - Details on the license [can be found here](LICENSE.md) diff --git a/index.js b/index.js index 82dd2fd..0805d8f 100644 --- a/index.js +++ b/index.js @@ -1,10 +1,19 @@ +// curry the callWithReorderedArgs function +const flippable = curry(callWithReorderedArgs) + +// export argument-flippable auto-curried methods +exports.map = flippable(curry(map)) +exports.filter = flippable(curry(filter)) +exports.reduce = flippable(curry(reduce)) +exports.forEach = flippable(curry(forEach)) + /** * Iterates over an object's keys, applying a transform function to each value. - * @param {Object} obj - the initial object * @param {Function} transform - the transform function. Receives `(value, key, index, object)` as arguments. + * @param {Object} obj - the initial object * @return {Object} - the new object */ -exports.map = function map (obj, transform) { +function map (transform, obj) { let res = {} let index = 0 for (let [val, key] of entries(obj)) { @@ -15,11 +24,11 @@ exports.map = function map (obj, transform) { /** * Iterates over an object's keys, applying a predicate function that determines whether the current key will appear in the returned object. - * @param {Object} obj - the initial object * @param {Function} predicate - the predicate function. Receives `(value, key, index, object)` as arguments. Should return `true` or `false`. + * @param {Object} obj - the initial object * @return {Object} - the new object */ -exports.filter = function filter (obj, predicate) { +function filter (predicate, obj) { let res = {} let index = 0 for (let [val, key] of entries(obj)) { @@ -32,10 +41,10 @@ exports.filter = function filter (obj, predicate) { /** * Iterates over an object's keys, calling an iterator function on each pass. - * @param {Object} obj - the initial object * @param {Function} iterate - the iterator function. Recives `(value, key, index, object)` as arguments. + * @param {Object} obj - the initial object */ -exports.forEach = function forEach (obj, iterate) { +function forEach (iterate, obj) { let index = 0 for (let [val, key] of entries(obj)) { iterate(val, key, index++, obj) @@ -44,12 +53,12 @@ exports.forEach = function forEach (obj, iterate) { /** * Iterates over an object's keys, applying a reducer function on each pass that reduces the current parameters into a single value. - * @param {Object} obj - the initial object. * @param {Function} reducer - the reducer function. Receives `(accumulator, value, key, index, object)` as arguments. * @param {*} accumulator - a value to accumulate results into. + * @param {Object} obj - the initial object. * @return {*} - the combined accumulated value. */ -exports.reduce = function reduce (obj, reducer, accumulator) { +function reduce (reducer, accumulator, obj) { let index = 0 for (let [val, key] of entries(obj)) { accumulator = reducer(accumulator, val, key, index++, obj) @@ -67,3 +76,28 @@ function * entries (obj) { yield [obj[key], key] } } + +/** + * Calls a function with supplied arguments - if first argument (`obj`) is not a function, it is moved to the end of the argument list. + * @param {Function} fn - a function to call + * @param {*} firstArg - the first argument + * @param {*} args... - the rest of the arguments + * @return {*} - the result of calling `fn()` with supplied args + */ +function callWithReorderedArgs (fn, firstArg, ...args) { + return typeof firstArg === 'function' + ? fn(firstArg, ...args) + : fn(...args, firstArg) +} + +/** + * Accepts a function, returning a function that partially-applies itself when there are missing arguments. + * @param {Function} fn - the function to curry + * @return {Function} - the curried function. + */ +function curry (fn) { + const partial = (...args) => args.length < fn.length + ? (...partialArgs) => partial(...args.concat(partialArgs)) + : fn(...args) + return partial +} diff --git a/test/curried.js b/test/curried.js new file mode 100644 index 0000000..42514fa --- /dev/null +++ b/test/curried.js @@ -0,0 +1,31 @@ +const test = require('ava') +const {map, reduce, filter, forEach} = require('..') +const obj = { foo: 'bar', doge: 'wow' } + +test('forEach', (t) => { + t.plan(2) + const fn = forEach((v, k) => t.truthy(obj[k] === v)) + fn(obj) +}) + +test('map', (t) => { + const fn = map((v, k) => v.toUpperCase()) + const mapped = fn(obj) + t.truthy(mapped.foo === 'BAR') + t.truthy(mapped.doge === 'WOW') +}) + +test('filter', (t) => { + const fn = filter((v, k) => k !== 'doge') + const filtered = fn(obj) + t.truthy(filtered.foo === 'bar') + t.falsy(filtered.doge === 'wow') +}) + +test('reduce', (t) => { + const fn = reduce((m, v, k) => m.push(v) && m, []) + const reduced = fn(obj) + t.truthy(reduced.length === 2) + t.truthy(reduced[0] === 'bar') + t.truthy(reduced[1] === 'wow') +}) diff --git a/test/functional.js b/test/functional.js new file mode 100644 index 0000000..fd2103d --- /dev/null +++ b/test/functional.js @@ -0,0 +1,27 @@ +const test = require('ava') +const {map, reduce, filter, forEach} = require('..') +const obj = { foo: 'bar', doge: 'wow' } + +test('forEach', (t) => { + t.plan(2) + forEach((v, k) => t.truthy(obj[k] === v), obj) +}) + +test('map', (t) => { + const mapped = map((v, k) => v.toUpperCase(), obj) + t.truthy(mapped.foo === 'BAR') + t.truthy(mapped.doge === 'WOW') +}) + +test('filter', (t) => { + const filtered = filter((v, k) => k !== 'doge', obj) + t.truthy(filtered.foo === 'bar') + t.falsy(filtered.doge === 'wow') +}) + +test('reduce', (t) => { + const reduced = reduce((m, v, k) => m.push(v) && m, [], obj) + t.truthy(reduced.length === 2) + t.truthy(reduced[0] === 'bar') + t.truthy(reduced[1] === 'wow') +}) diff --git a/test/index.js b/test/imperative.js similarity index 100% rename from test/index.js rename to test/imperative.js