Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add functional auto-curried syntax support #3

Merged
merged 5 commits into from
Jun 16, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 67 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,99 @@

[![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 and 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
```sh
$ 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')

const obj = { foo: 'bar', wow: 'doge' }

map(obj, (val, key) => val.toUpperCase())
map(obj, (val) => val.toUpperCase())
// { foo: 'BAR', wow: 'DOGE' }

reduce(obj, (accum, val, key) => accum[val.toUpperCase()] = key && accum }, {})
reduce(obj, (acc, val, key) => (acc[key.toUpperCase()] = val, acc), {})
// { FOO: 'bar', WOW: 'doge' }

filter(obj, (val, key) => !key === 'foo')
filter(obj, (val, key) => key !== 'foo')
// { wow: 'doge' }

forEach(obj, console.log.bind(console))
// logs out all the values
// bar foo 0 { foo: 'bar', wow: 'doge' }
// doge wow 1 { foo: 'bar', wow: 'doge' }
```

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.
#### 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' }

**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.
const upcaseValues = map((val) => val.toUpperCase())
upcaseValues(obj)
// { foo: 'BAR', wow: 'DOGE' }

const upcaseKeys = reduce((acc, val, key) => (acc[key.toUpperCase()] = key, acc), {})
upcaseKeys(obj)
// { FOO: 'bar', WOW: 'doge' }

const ignoreFoo = filter((val, key) => key !== 'foo')
ignoreFoo(obj)
// { wow: 'doge' }

const logValues = forEach(console.log.bind(console))
logValues(obj)
// bar foo 0 { foo: 'bar', wow: 'doge' }
// doge wow 1 { foo: 'bar', wow: 'doge' }
```

### Method Signature

- Each callback has a method signature of `(value, key, index, object)` with the exception of `reduce`.
- `value` is the current key's value
- `key` is the current key's name
- `index` is the 0-based index of the current key
- `object` is the original object.
- `reduce` has a method signature of `(accumulator, value, key, index, object)`.
- `accumulator` is any initial value onto which you want to iteratively reduce from `object`.

### Differences in `reduce`

In `objectfn`, the act of passing an accumulator to the `reduce` method is _required_, which is better for readability/accessibility (developer intentions are made more obvious), has no immediate disadvantages and is one of the two reasons `objectfn` is able to support both functional and imperative syntaxes.

This means that this will work:

```js
let obj = { one: 1, two: 2, three: 3, four: 4 }
reduce(obj, (accum, val) => accum + val, 0) // => 10
reduce(obj, (acc, val) => acc + val, 0) // => 10
```

But this will not:
Expand All @@ -56,6 +104,14 @@ let obj = { one: 1, two: 2, three: 3, four: 4 }
reduce(obj, (prevVal, currVal) => prevVal + currVal) // => wat?
```

### Binding `this`

`objectfn` offers no mechanism for binding the `this` context of the callback via the last parameter. This is one of two reasons why `objectfn` is able to support both functional and imperative syntaxes. If you want this behavior, it is still possible (and 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.