Skip to content

Commit

Permalink
add functional auto-curried syntax support
Browse files Browse the repository at this point in the history
  • Loading branch information
zspecza committed Jun 14, 2016
1 parent ee73788 commit a4b1d0d
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 13 deletions.
56 changes: 51 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,31 @@

[![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?

I wanted a library that has no dependencies and gives me the basic map/reduce/filter for use on objects. Any existing library I found has boatloads of dependencies, provides tons more extra tools, and/or is unmaintained. So here's `ObjectFn`, just for you!

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')
Expand All @@ -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:

Expand All @@ -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)
Expand Down
50 changes: 42 additions & 8 deletions index.js
Original file line number Diff line number Diff line change
@@ -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)) {
Expand All @@ -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)) {
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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
}
31 changes: 31 additions & 0 deletions test/curried.js
Original file line number Diff line number Diff line change
@@ -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')
})
27 changes: 27 additions & 0 deletions test/functional.js
Original file line number Diff line number Diff line change
@@ -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')
})
File renamed without changes.

0 comments on commit a4b1d0d

Please sign in to comment.