Giter Club home page Giter Club logo

functional-promises's Introduction

Functional Promises

Build Status GitHub package version GitHub stars

New Docs

Summary

The Functional Promises library is a Fluent API supporting a specific Function Chaining technique (using composition).

Note: FP doesn't replace or extend Promises. It uses them.

There are many names for this general pattern. Including Collection Pipeline and Promise Chain. The emphasis here is a seamless async/sync Developer Experience.

I call this pattern a Functional River - your data is the water, and your functions describe its path or riverbed.

Advantages

  • Only 400 Lines of Source & 3Kb compressed
  • Easily handle Sync, Async, Events, Promises, and Callbacks.
  • Familiar methods, including Array.prototype.map, [].filter(), [].find(), [].some(), etc.
  • Create Monads in JavaScript (so far as they contain side-effects to a sequence of instructions).
  • Point-free code is easily achieved (no temp variables).
  • Higher code test coverage w/ less repetitive code in tests.
  • Use the best features from multiple programming styles: a little imperative, plenty functional, a pinch of OOP, yet still resembling declarative methods!!!

Library Comparison

Total Lines of Code (LoC) calculated using cloc CLI utility.

LoC #'s included because a smaller surface === fewer places bugs can hide.

Library Main deal Files Lines of Code .min.js kB
Functional Promise v1.8.1 Sync & Async Chains 8 375 12 Kb (3Kb compressed)
Bluebird v3.5.1 Promises Replacement 38 5,188 80 Kb
RxJS v5.5.6 Observables Chaining 458 12,266 150 Kb
IxJS v2.3.4 [Async]Iterable Chaining 521 12,366 145 Kb

Admittedly IxJS/RxJS have a far larger API than FP also some behavior in RxJS/IxJS may never be added. Currently however there is a lot of overlap with FP (plus more planned).

The table above show FP is roughly 1/30th the LOC (lines of code) in R/IxJs. FP's bundle size is about 10% the size of either RxJS/IxJS.

BluebirdJS and FP have roughly the same number of API methods, yet Bluebird has a fair bit more code.

To be clear: Bluebird and RxJS/IxJS are amazing. Their interface/designs has been very influential on FP.

Note: R/IxJS's hyper-modular design also allows for bundles to be lots smaller (though using quite different syntax, either .pipe(...) or ix/iterable/ix/add/...).

Installation

npm install functional-promises

Getting Started

Use one of the following:

const FP = require('functional-promises')
// or:
import FP from 'functional-promises'

Quick Examples

Using .map()

FP.resolve([1, 2, 3, 4, 5])
  .map(x => x * 2)
  .map(x => x * 2)
  .then(results => {
    // results === [4, 8, 12, 16, 20]
  })

Handling Events

Create function chains to handle the case where promises don't fit very naturally.

For example streams & event handlers must (usually) support multiple calls over time.

Here's how FP.chain() and FP.chainEnd()/FP.listen(obj, event) help you handle this like a pro:

const button = document.getElementById('submitBtn')
FP.chain() // start a chain
  .then(({target}) => { // destructure 'target' from the `event`
    target.textContent = 'Clicked!'
  })
  .listen(button, 'click') // end the repeatable chain, started at `.chain()`

Development

git clone [email protected]:functional-promises/functional-promises.git
cd functional-promises
npm install
npm test

Thanks to several influencial projects: RxJS, IxJS, Bluebird, asynquence, FantasyLand, Gulp, HighlandJS, et al.

Special thanks to Kyle Simpson, Eric Elliot, and Sarah Drasner for their work for the OSS community, as well as their advice & encouragement.

functional-promises's People

Contributors

brookspatton avatar dannyfritz avatar dependabot[bot] avatar dhigginbotham avatar html5cat avatar justsml avatar kaelinator avatar luishrd avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

functional-promises's Issues

Add alias `.pipe()` to `.chain()`

Related to #35

pipe is the operation used to compose Left-to-Right sequences in other langs/libs.

Apply any options object's keys as modifier function(s) w/ specified value.

// syntax ideas:

// .pipe(options) .line()
FP.pipe()
  .map(...).tap(...).get(...)
  .line()


// .pipe(options) .pipeEnd()
FP.pipe()
  .map(...).get(...)
  .pipeEnd()

Consider alternate semantics to .chainEnd()

Consider alternatives to .chain() / .chainEnd()

See details and examples for current chaining API

Outline of changes

  1. Instead of .chain() consider: .build(), .create(), .record(), or .pipeline()
  2. Instead of limiting to .chainEnd():
    • .toPromise() returns a function which runs the Chain of functions, returning the final result as a promise.
    • .toCallback()(err, value)
    • .toListener(object, events)

Avoid Semver Major +1

  • Alias .chainEnd to .toPromise()
  • Alias .chain() w/ .build()
const loud = value => (''+ value).toUpperCase()

const chainFn = FP.build()
    .then(loud)
    .tap(console.log)
    .then(s => s + '!!!')
    .toPromise()

chainFn('dan levy')
  .then(s => console.log(`Hi ${s}`)) // Hi DAN LEVY!!!


const callbackFn = FP.chain()
  .then(loud)
  .toCallback()

callbackFn('dan levy', (err, upperStr) => {
  console.log(`Hello ${upperStr}`) //=> Hello DAN LEVY
})

const listenFn = FP.chain()
  .get('target')
  .get('textContent')
  .then(loud)
  .tap(str => console.log('Loud: ' + str))
  .toListener()

listenFn(statusFooter, 'click', 'mouseover')

BUG: Combining .delay with .concurrency breaks

Reproduce Example:

const started = Date.now()

FP.resolve([1, 2, 3, 4])
  .concurrency(10)
  // now only 1 map() callback happens at a time
  .map(num => {
    return FP
      .delay(50)
      .resolve(num)
  })
  .then(() => {
    const runtime = Date.now() - started
    console.log(`Delayed ${runtime}ms.`)
    console.log(`Success: ${runtime >= 200}`)
  })

Add 3rd party Promise wrapper

Proposed .wrap() FP conversion utility method.

const fetch = FP.wrap(fetch);
// Now you can call the FP enhanced`fetch`

fetch('/profile', {method: 'GET'})
  .thenIf(
    res => res.ok, // Check for 
    res => res.json(), // Success, so pass along the JSON-parsed body
    res => Promise.reject(new Error('Profile GET Failed'))) // Fails here if response not `ok`
  .get('avatar') // Get the response JSON object's `avatar` key value
  .then(avatarUrl => imageElem.src = avatarUrl)

Deep chaining loses the `FP` instance wrappers!

PoC:

const fetch  = require('isomorphic-fetch');

FP.resolve(fetch('https://api.github.com/users/justsml'))
  .tap(res => console.log(`github user req ok? ${res.ok}`))
  .then(res => res.json())
  .then(data => console.log('data', data.avatar_url))
  // NEXT LINE SHOULD WORK!!!
  .tap(data => console.log('github result', data))
  //.then(data => console.log(data))

image

Add FP.flatMap()

Add .flatMap()

Should we limit depth? Or does that force our implementation to be recursive (to behave correctly)? Is that the way to do it anyway?

Call stack limits a concern?

error in findIndex example from site

Hello,

I try this example on your site
but the findIndex return only 0 (normally here he should return 3)

const rawData = [-99, null, undefined, NaN, 0, 20,40,50,'99']

// Async compatible (not needed in this simple example)
FP.resolve(rawData)
  .filter(x => x)         // truthiness check = [-99, "99"]
  .map(x => parseInt(x))  // convert to numeric [-99 99]
  .findIndex(n => n == 50) // is gte 1, idx = 1
  .then(index => {
    console.log(index)    // 0
  })

Thanks

Add test: `FP.all()`

Requirements

  • FP.all(Object)
  • FP.all(Array)
  • FP.all(Object, Object) - error expected
  • FP.all(Array, Array) - error expected

Update Docs

  • Update docs w/ new Array error handling.
  • Add usage about flatMap()

Extend API: `.get`

Requirements

const user = FP.resolve({username, password})
const userArr = FP.resolve([123, 'Smith', 'Agent'])
  • Array Arg: user.get(['username', 'email']).then(({username, email}) => {...})
  • List of Args: user.get('username', 'email').then(({username, email}) => {...})
  • Mapping Array Indices: userArr.get(2, 1).then([first, last]=> first + ' ' + last)

Extend API: `.set`

Requirements

const user = FP.resolve({username, password})
const userArr = FP.resolve([123, 'Smith', 'Agent'])
  • Array Arg: user.set(['password'], null) // {username, password: null}
  • List of Args: user.set('password', 'email', null) // {username, password: null, email: null}
  • Mapping Array Indices: userArr.set(0, -1) // assert.deepEqual(arr[0], -1))

Array methods can swallow `.catch`'s

Credit/Report by: https://github.com/donabrams

Appears tied to the .chain() semantics.

TODO:

  • Add needed unit tests

Hacked up check:

// https://runkit.com/justsml/fp-bug-error-handling

const FP = require('functional-promises')

const errorTwoMaps = FP.chain()
  .map(x => Promise.reject(x))
  .map(x => Promise.reject(x))
  .catch(e => {
    console.log(e); 
    return new Error("silly");
  })
  .chainEnd();

const errorOneMap = FP.chain()
  .quiet(0)
  .map(x => Promise.reject(x))  
  .chainEnd();

errorOneMap([41])
    .then(console.info.bind(console, 'ONE:'))
    .catch(console.error.bind(console))

errorTwoMaps([42])
    .then(console.info.bind(console, 'TWO:'))
    .catch(console.error.bind(console))

See: https://runkit.com/justsml/fp-bug-error-handling

image

Add timeout support

In order to minimize complexity & reduce risk of memory leaks I'm proposing a change from Bluebird's implementation.

Progress

  • Single modifier function
  • Chain option support

.timeout() should be a modifier/prefix-method for thenable methods - this means it only affects the subsequent thenable.

Ideally it should emit a timeout event on the FP instance before rejecting - help intelligently prevent double-calling resolve/reject methods.

If scoping the timeout over multiple steps is wanted, extending the .chain() should should be the foundation.

Apply timeout to next thenable

Difficulty 1 to 10 (10=hard): 2

const Users = mongoose.collections('users')

// initial non-FP promise wrapped with static helper `FP.timeout(fn/promise, msec)`
const getUserSocialData = id => FP.timeout(Users.find({_id: id}), 5000)
  .timeout(1000) // set 1 sec limit
  .then(user => api.getAllSocialData(user)) // 1 sec limit
  .get('socialAccounts') // extract
  .map(account => loadSupportHistory(account)) // no timeout limit

Apply timeout over multiple steps

Uses .chain({timeout: msec}) and .run(input)

Difficulty 1 to 10 (10=hard): 6

const Users = mongoose.collections('users')

const getUserSocialData = id => FP.chain({timeout: 10000})
  .then(() => Users.find({_id: id}))
  .then(user => api.getAllSocialData(user))
  .get('socialAccounts') // extract
  .map(account => loadSupportHistory(account))
  .run() // optional arg would be passed to the first thenable in the chain

Improve Error Handling

Allow for soft errors (so .map() can continue from errors w/o extra nesting).

Essentially syntactic sugar so this:

FP.resolve([1, 0])
  .map(count => FP.resolve(count).then(c => 1 / c).catch(err => 'Error ignored!'))

Can become more like:

FP.resolve([1, 0])
  .quiet()
  .map(count => FP.resolve(count).then(c => 1 / c))
  .catch(err => 'Error ignored!')

.quiet() will be a prefix/modifier FP function.

Prefix functions modify the subsequent .then()-based method.

Another example is the .concurrency(threadLimit) modifier (used with FP.Array methods).

UnhandledRejection error in `FP.all()`

See Error here: https://repl.it/@justsml/fp-defect-proof#index.js

const FP = require('functional-promises')
const iThrowThings = async () => {
  throw new Error('๐Ÿ”ช๐Ÿ”ช๐Ÿ”ช๐Ÿ”ช๐Ÿ”ช๐Ÿ”ช๐Ÿ”ช')
}

const brokenPromises = () => FP.resolve([{ tears: true }])
  .map(iThrowThings)
  .then(() => {
    console.log('whaaaa')
  }).catch(ex => { throw ex })

try {
  brokenPromises()
} catch (ex) {
  console.log('dumb me')
}

Throws:

(node:82) UnhandledPromiseRejectionWarning: Error: ๐Ÿ”ช๐Ÿ”ช๐Ÿ”ช๐Ÿ”ช๐Ÿ”ช๐Ÿ”ช๐Ÿ”ช
(node:82) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 2)
(node:82) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

From @dhigginbotham

import FP or require FP retun a empty object

Hello,

I want try your libray , but after make npm install functional-promise,
import or require return a empty object.

I make the test on codesandbox.io , stackblitz.com and on I create a project via npm (create-react-app)

I miss something ?

Thanks

Add Stream Support

Handling streams with chaining syntax is popular in libs like http://highlandjs.org/.

Promises provide a powerful chaining technique.

Combining the two rarely looks like it was meant to be.

I'm trying to find a convenient pattern to use with functional promise chains, here's one pattern I think might work well:

function findInLogs(searchText, logName = 'system') {
  return FP.stream(fs.createReadStream(`/var/log/${logName}`, {encoding: 'utf8'}))
  .on('data', data => {
    if (data.indexOf(searchText) > -1) {
      this.resolve(`Found ${searchText} in logs!`);
    }
  })
  .on('error', err => this.reject(err))
  .on('finish', () => this.resolve(`Searched text not found!`));
}

Fix Node's `Error` constructor bug

Fix error constructor bug (for Node v4-v9)

more info

        super(message); // 'Error' breaks prototype chain here
        Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain

Add throttling options to the `.chain()` functionality?

Side note: This might also might be a natural place for timeout see: #7

// options ref: https://lodash.com/docs/4.17.10#throttle

// proposed options for .chain:
const doLotsOfThings = FP.chain({throttle: { wait: 1000, leading: false, trailing: true }})
  .then(doThing)
  .tap(logThing)
  .map(transformThings)
  .tap(() => console.log('end chain'))
  .chainEnd()

doLotsOfThings()
// output: end chain

doLotsOfThings()
// wait: ~1,000 milliseconds
// output: end chain
doLotsOfThings()
// wait: ~2,000 milliseconds
// output: end chain

Alternate 1/3

This is the zero effort method:

const throttle = require('lodash/fp/throttle');
// lodash auto curries, so we just wrap chain with `throttle(1000, fn)`

const doLotsOfThings = throttle(1000, FP.chain()  // < ... yea, any better?
  .then(doThing)
  .tap(logThing)
  .map(transformThings)
  .chainEnd())

Alternate 2/3

Add arguments chain end call: .chainEnd({apply: function[s]})
Not sure on naming, wrappers vs apply (as in Function.apply)???

const throttle = require('lodash/fp/throttle');
// lodash auto curries, so we just pass `throttle(1000)` into the .chainEnd()

const doLotsOfThings = FP.chain()
  .then(doThing)
  .tap(logThing)
  .map(transformThings)
  .chainEnd({wrappers: [throttle(1000)]}) // < ... yea, any better?

Alternate 3/3

Similar to .chainEnd() add specific helper .chainWith(...functions)

Add arguments chain end call: .chainWith(...function[s])
Feeling better about the naming...

Note to uptight Functional Programmer purists: I'm intentionally avoiding your jargon... ๐Ÿ˜‡ Please don't Functor @ me bro.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.