Giter Site home page Giter Site logo

fun-task's Introduction

fun-task* Build Status Coverage Status

An abstraction for managing asynchronous code in JS.

* The name is an abbreviation for "functional task" (this library is based on many ideas from Functional Programming). The type that library implements is usually referred to in the documentation as just "Task".

Installation

NPM

npm install fun-task
// modern JavaScript
import Task from 'fun-task'

// classic JavaScript
var Task = require('fun-task')

CDN

<script src="https://unpkg.com/fun-task/umd/funTask.js"></script>
<script>
  var Task = window.FunTask
</script>

What is a Task?

Task is an abstraction similar to Promises. The key difference is that a Task represents a computation while a Promise represents only a result of a computation. If we have a Task we can: start the computation; terminate it before it's finished; or wait until it finishes, and get the result. While with a Promise we can only get the result. This difference doesn't make Tasks better, they are just different, we can find legitimate use cases for both abstractions. Let's review it again:

If we have a Task:

  • We can start the computation that it represents (e.g. a network request)
  • We can choose not to start the computation and just throw task away
  • We can start it more than once
  • While computation is running, we can notify it that we're not interested in the result any more, and as a response computation may choose to terminate itself
  • When computation finishes we get the result

If we have a Promise:

  • Computation is already running (or finished) and we don't have any control of it
  • We can get the result whenever it's ready
  • If two or more consumers have a same Promise they all will get the same result

The last item is important. This might be an advantage of Promises over Tasks. If two consumers have a same Task, each of them have to spawn their own instance of the computation in order to get the result, and they may even get different results.

What is a computation?

function computation(onSuccess, onFailure) {
  // ...
  return () => {
    // ... cancellation logic
  }
}

From Task API perspective, computation is a function that accepts two callbacks. It should call one of them after completion with the final result. Also a computation may return a function with cancellation logic, or it can return undefined if particular computation has no cancellation logic.

Creating a Task from a computation is easy, we just call task = Task.create(computation). This is very similar to new Promise(computation), but Task won't execute computation immediately, the computation starts only when task.run() is called.

Documentation

The NPM package ships with Flow definitions. So you can do something like this if you use Flow:

// @flow

import Task from 'fun-task'

function incrementTask<F>(task: Task<number, F>): Task<number, F> {
  return task.map(x => x + 1)
}

Specifications compatibility

Task is compatible with Fantasy Land and Static Land implementing:

Development

npm run lobot -- --help

Run lobot commands as npm run lobot -- args...

fun-task's People

Contributors

codincat avatar davidchase avatar daytonlowell avatar ericelliott avatar noor avatar npmcdn-to-unpkg-bot avatar rpominov avatar safareli 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  avatar  avatar  avatar

fun-task's Issues

Similar projects

This became a habit for me to open such issue in all of my recent projects. Feel free to add links or discuss other projects here.

Here are two links for a start:

Two major differences of this project compared to listed above, is cancelation and runAndCatch feature. Not sure about cancelation in data.task though, but I think it at least works differently, or maybe data.task doesn't support it. Also there are number of minor differences.

Passing context to computations when calling run()

Not sure whether this is a good idea or not, but it might be...

The idea is to make this possible:

const task = Task.create((onSucc, onFail, context) => {
  // context === 123
  // ...
}).chain(x => Task.create((onSucc, onFail, context) => {
  // context === 123
  // ...
})
task.run(onSucc, onFail, 123)

This might be useful for testing for example. We could pass an API for data fetching as context, and during testing we could use a mocked fetching API.

finally callback in run?

If we add this we should probably pass callbacks as an object, otherwise there are to many args (especially in runAndCatch, and if we add context feature):

// Instead of this
task.run(onSuccess, onFailure, finally)

// we should do this (need to think more about names)
task.run({onSuccess, onFailure, finally})

Should race be symmetrical to parallel?

It's easier to show using type signatures. Should it be like the following?

parallel(tasks: Array<Task<S, F>>): Task<Array<S>, F>
race    (tasks: Array<Task<S, F>>): Task<S, Array<F>>

The description of race would change from:

Given array of tasks creates a task that completes with the earliest successful or failure value

to:

Given array of tasks creates a task that completes with the earliest successful value or, if all tasks fail, with array of failure values.

I don't have strong opinion yet, just discovered that we could make parallel and race symmetrical, and thought that maybe it's a good idea. Need to consider all the use-cases...

Fun-task in react/redux

Hey!

Fun-task seems really good, can't wait to play around with it. Do you have any experience using it in a react & redux application or seen other people do it?

Thanks for a cool library

Promise.filter

Say I have an array of items. And I want to filter those base on an asynchronous action.

With a bluebird promise I can do this:

Promise.filter(xs, x =>
   asyncConditionFunction(x)
)
.then(filteredXs => console.log(filteredXs))

Do you know of any nice pattern do convert this to tasks?
When I tried it got messy quite quickly. Lets assume asyncConditionFunction return a task instead:

Task.of(xs)
.chain(xs =>
  Task.paralell(xs.map(x => asyncConditionFunction(x))
   .map(arrayWithBooleans => xs.filter((x, i) => arrayWithBooleans[i])) // This feels so wrong.
)
.map(xs => console.log(xs)) // xs has been filtered

Do you know any nicer pattern?

Should ap be parallel?

Strictly speaking this would violate Fantasy Land and Static Land requirements.

Here is an explanation I wrote in Fantasy Land gitter room recently:

So the spec require:

If a data type provides a method which could be derived, its behaviour must be equivalent to that of the derivation (or derivations).

Also spec says that ap can be derived as function(m) { return this.chain(f => m.map(f)); }. In case of tasks the derived ap will execute tasks in sequence. So basically if provided ap is parallel then it's not equivalent to derived ap. Of course we could choose to consider sequential/parallel execution as an optimization unrelated to equivalence, since we basically can define equivalence as we like. But I think that would be wrong, sequential/parallel execution is related to equivalence.

But I can't think of actual practical consequence of this violation. And on the other hand it would be nice if traverse of types other than array would execute tasks in parallel (we have built-in kinda traverse for arrays — Task.parallel()).

Also other libraries like Fluture or data.task have parallel ap and seem to not have any trouble with it. @Avaq @robotlolita Maybe you can share some thoughts?

filter method

The idea is that task.filter(predicate) will complete only when original tasks completes and the result satisfies the predicate. If the original task completes but result don't satisfies the predicate the result task will never complete.

Here is an example use case:

Task.race([
  possibleResult1.filter(isGoodResult),
  possibleResult2.filter(isGoodResult),
  possibleResult3.filter(isGoodResult),
  rejectsAfterSomeTime, // timeout
]).run(handleGoodResult, handleTimeout)

Run-time arguments checks

We should check arguments to be of correct types in user facing methods (i.e. public API methods) and throw early.

Fusion

map(f).map(g) -> map(g . f)
race([race([a, b]), c]) -> race([a, b, c])
race([empty(), a]) -> a
empty().map() -> empty()
etc.

Also we should check Fantasy Land laws to find more fusion opportunities.

Taskify a function

When working with promises I've found it useful to consume functions regardless of them returning a plain value or a promise. Thus I can always chain these arbitrary functions in "promise-style" f1().then(f2). ... i.e. with Ramda R.composeP(f1, f2).

I think it would be useful to have this for Tasks as well:

var R = require('ramda'); // just for my convenience writing this implementation - sorry!
var toPromise = Promise.resolve.bind(Promise);  // helper
// takes a function func which returns a value, Promise or Task and returns a function which
// will return a Task.
var taskify = function(func){
    return R.compose(R.unless(R.is(Task), R.compose(Task.fromPromise, toPromise)) , func);
};
// examples:
taskify( (x, y) => x + y )(8, 9).runAndLog(); // -> Success: 17
taskify( (x, y) => Promise.resolve(x + y) )(8, 9).runAndLog();  // -> Success: 17
taskify( (x, y) => Task.of(x + y) )(8, 9).runAndLog();   // -> Success: 17

What do you think?

Memoization (a method that creates a Promise-like task from normal task)

const memoizedTask = task.memoize()

The memoizedTask will run() original task at most once, even if we do memoizedTask.run() many times. If we cancel a run of a memoized Task it will work as unsubscription and will not cancel original task.

Memoized Task is almost like Promise. We can give it to multiple consumers, and they'll all get the same result, also computation will run only once. The differences from Promise is that it's still lazy (runs original task only after the first memoizedTask.run()), and that we still can unsubscribe.

chain-based recursion

Currently we don't support infinite chain-based recursion like the following. Library will consume more and more memory as program runs in such case.

// suppose this task never fails
const someTask = ...
const recur = () => someTask.chain(recur)
recur()

Things we should try:

  • Create a method that basically does exactly what is going on above, so we could do someTask.recur(). This method still be usable if someTask fails (recursion won't be infinite)
  • Create similar method but with orElse instead of chainsomeTask.retryUntilSuccess(). Also useful if task succeeds eventually.
  • Try to optimize these methods using low-level API and make them support infinite recursion.

Progress/step events

Will tasks be able to report on the progress of measurable, potentially long-running operations, like file uploads and downloads?

I see progression as the domain of observables, not tasks/promises, but what's your opinion?

toPromise method

I think this method will run the task and give us a promise that represents the result.

toEither method

It would turn Task<S, F> into Task<{success: S} | {failure: F}, any>*.

* Would be cool to use empty instead of any here, but Flow doesn't support it yet: facebook/flow#2225

I've already run into two cases where it could be useful:

In Task.do.

Task.do(function* () {
  const {success, failure} = yield doStuff().toEither()
  // ...
})

In combination with toPromise (task.toEither().toPromise()) because Promise's error path isn't really good to use for expected failures, it's better to reserve it only for uncaught exceptions / bugs (update: or we probably should do it automatically in toPromise #4 (comment)).

The name

I'm not a fun of toEither name, would appreciate a help here. Other names I've considered so far:

  • join — it's better to reserve this name for monadic join (chain(x => x))
  • reduce — conflicts with Fantasy Land's Foldable

Also both join and reduce not very intuitive i.e., bad names either way.

The idea of toEither is that {success: S} | {failure: F} basically poor man's Either so in other words we transform Task<S, F> into Task<Either<S, F>, empty>. The problems are: 1) this explanation is required to understand the name, 2) strictly speaking the name should be toTaksOfEither.

Explanation of exceptions has incorrect image

image
at https://github.com/rpominov/fun-task/blame/master/docs/exceptions.md#L32-L34

In the first section of the image the x <= 0 true case in the code shows that you alert(z + 5), but the image shows that you alert(y + 5)

In the second section of the image, the x > 0 should still lead to y = 10, or the color should be red to indicate the failure case of the first conditional branch. And then the green path for truth should lead to the alert(y + 5) path.

Also, you state that there are two expected results of the program, but your initial if condition doesn't set z or y when x = 0, which is "technically" possible via the Math.random() spec...

Better Flow type for Task.parallel()

It should be overloaded function with signatures like so:

([Task<S, F>]): Task<[S], F>
([Task<S1, F1>, Task<S2, F2>]): Task<[S1, S2], F1 | F2>
([Task<S1, F1>, Task<S2, F2>, Task<S3, F3>]): Task<[S1, S2, S3], F1 | F2 | F3>
// ...
(Array<Task<S, F>>): Task<S[], F>

The problem is we'll have to duplicate all types from Task class in a declaration facebook/flow#2260 (comment)

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.