Skip to content

Latest commit

 

History

History
516 lines (386 loc) · 10.6 KB

api-reference.md

File metadata and controls

516 lines (386 loc) · 10.6 KB

Task API Reference

Task.create(computation)

Creates a Task from a computation. Computation is a function that accepts two callbacks. It should call one of them after completion with the final result (success or failure). Also a computation may return a function with cancellation logic or it can return undefined if there is no cancellation logic.

const task = Task.create((onSuccess, onFailure) => {
  // ...
  return () => {
    // cancellation logic
  }
})

// The computation is executed every time we run the task
const cancel = task.run({
  success(result) {
    // success result goes here
  },
  failure(result) {
    // failure result goes here
  },
})

// If we cancel the task the cancellation logic from computation
// will be executed (if provided)
cancel()

Here is some runnable example:

const wait5sec = Task.create(onSuccess => {
  const timeoutId = setTimeout(() => {
    onSuccess('5 seconds')
  }, 5000)
  return () => { clearTimeout(timeoutId) }
})

wait5sec.run({
  success(timeWaited) {
    console.log(`We've waited for ${timeWaited}`)
  },
})

// > We've waited for 5 seconds

After cancellation or completion the onSuccess and onFailure callbacks become noop. Also if cancel called second time or after a completion the cancelation logic won't be executed.

Task.of(value)

Creates a task that resolves with a given value.

Task.of(2).run({
  success(x) {
    console.log(`result: ${x}`)
  },
})

// > result: 2

Task.rejected(error)

Creates a task that fails with a given error.

Task.rejected(2).run({
  failure(error) {
    console.log(`error: ${error}`)
  },
})

// > error: 2

Task.empty()

Creates a task that never completes.

Task.empty().run({
  success(x) {
    // callback never called
  },
  failure(error) {
    // callback never called
  },
})

task.map(fn)

Static alias: Task.map(fn, task)

Transforms a task by applying fn to the successful value.

Task.of(2).map(x => x * 3).run({
  success(x) {
    console.log(`result: ${x}`)
  },
})

// > result: 6

task.mapRejected(fn)

Static alias: Task.mapRejected(fn, task)

Transforms a task by applying fn to the failure value.

Task.rejected(2).mapRejected(x => x * 3).run({
  failure(error) {
    console.log(`error: ${error}`)
  },
})

// > error: 6

task.bimap(fFn, sFn)

Static alias: Task.bimap(fFn, sFn, task)

Transforms a task by applying fFn to the failure value or sFn to the successful value.

Task.of(2).bimap(x => x, x => x * 3).run({
  success(x) {
    console.log(`result: ${x}`)
  },
})

// > result: 6
Task.rejected(2).bimap(x => x * 3, x => x).run({
  failure(error) {
    console.log(`error: ${error}`)
  },
})

// > error: 6

task.chain(fn)

Static alias: Task.chain(fn, task)

Transforms a task by applying fn to the successful value, where fn returns a Task.

Task.of(2).chain(x => Task.of(x * 3)).run({
  success(x) {
    console.log(`result: ${x}`)
  },
})

// > result: 6

The function can return a task that fails of course.

Task.of(2).chain(x => Task.rejected(x * 3)).run({
  failure(error) {
    console.log(`error: ${error}`)
  },
})

// > error: 6

task.orElse(fn)

Static alias: Task.orElse(fn, task)

Transforms a task by applying fn to the failure value, where fn returns a Task. Similar to chain but for failure path.

Task.rejected(2).orElse(x => Task.of(x * 3)).run({
  success(x) {
    console.log(`result: ${x}`)
  },
})

// > result: 6

task.recur(fn)

Static alias: Task.recur(fn, task)

task.recur(fn) is the same as task.chain(function f(x) { return fn(x).chain(f) }), but former is safe from infinite call stack growth and memory leaks.

Task.of(5).recur(x => {
  x // 5, 4, 3, 2, 1, 0
  return x === 0 ? Task.rejected('done') : Task.of(x - 1)
}).run({
  failure(x) {
    console.log(`result: ${x}`)
  },
})

// > result: done

Task.chainRec(fn, initial)

Implementation of Fantasy Land's ChainRec. Covers similar use-case as task.recur() but in a spec compatible way.

Task.chainRec((next, done, x) => {
  x // 5, 4, 3, 2, 1, 0
  return x === 0 ? Task.of(done('done')) : Task.of(next(x - 1))
}, 5).run({
  success(x) {
    console.log(`result: ${x}`)
  },
})

// > result: done

tX.ap(tFn)

Static alias: Task.ap(tFn, tX)

Applies the successful value of task tFn to to the successful value of task tX. Uses chain under the hood, if you need parallel execution use parallel.

Task.of(2).ap(Task.of(x => x * 3)).run({
  success(x) {
    console.log(`result: ${x}`)
  },
})

// > result: 6

task.concat(otherTask)

Static alias: Task.concat(task, otherTask)

Selects the earlier of the two tasks. Uses race under the hood.

const task1 = Task.create(suc => {
  const id = setTimeout(() => suc(1), 1000)
  return () => { clearTimeout(id) }
})

const task2 = Task.create(suc => {
  const id = setTimeout(() => suc(2), 2000)
  return () => { clearTimeout(id) }
})

task1.concat(task2).run({
  success(x) {
    console.log(`result: ${x}`)
  },
})

// > result: 1

Task.parallel(tasks)

Given array of tasks creates a task of array. When result task executed given tasks will be executed in parallel.

Task.parallel([Task.of(2), Task.of(3)]).run(
  success(xs) {
    console.log(`result: ${xs.join(', ')}`)
  },
)

// > result: 2, 3

If any of given tasks fail, the result taks will also fail with the same error. In this case tasks that are still running are canceled.

Task.parallel([Task.of(2), Task.rejected(3)]).run(
  failure(error) {
    console.log(`error: ${error}`)
  },
)

// > error: 3

Task.race(tasks)

Given array of tasks creates a task that completes with the earliest successful or failure value. After the fastest task completes other tasks are canceled.

const task1 = Task.create(suc => {
  const id = setTimeout(() => suc(1), 1000)
  return () => {
    console.log('canceled: 1')
    clearTimeout(id)
  }
})

const task2 = Task.create(suc => {
  const id = setTimeout(() => suc(2), 2000)
  return () => {
    console.log('canceled: 2')
    clearTimeout(id)
  }
})

Task.race([task1, task2]).run({
  success(x) {
    console.log(`result: ${x}`)
  },
})

// > canceled: 2
// > result: 1

Task.do(generator)

This is something like Haskell's do notation or JavaScritp's async/await based on generators.

You pass a generator that yiels and returns tasks and get a task in return. The whole proccess is pure, tasks are not being ran until the result task is ran.

Here is a not runnable but somewhat real-world example:

// gets user from our API, returns a Task
const getUserFromAPI = ...

// gets zip code for given address using 3rd party API, returns a Task
const getZipCode = ...

function getUsersZip(userId) {
  return Task.do(function* () {
    const user = yield getUserFromAPI(userId)
    if (!user.address) {
      return Task.rejected({type: 'user_dont_have_address'})
    }
    return getZipCode(user.address)
  })
}

// Same function re-written using chain instead of do
function getUsersZip(userId) {
  return getUserFromAPI(userId).chain(user => {
    if (!user.address) {
      return Task.rejected({type: 'user_dont_have_address'})
    }
    return getZipCode(user.address)
  })
}

getUsersZip(42).run({
  success(zip) {
    // ...
  },
  failure(error) {
    // The error here is either {type: 'user_dont_have_address'}
    // or some of errors that getUserFromAPI or getZipCode can produce
    // ...
  },
})

And here's some runnable example:

Task.do(function* () {
  const a = yield Task.of(2)
  const b = yield Task.of(3)
  return Task.of(a * b)
}).run({
  success(x) {
    console.log(`result: ${x}`)
  },
})

// > result: 6

task.run(handlers)

Runs the task. The handlers argument can contain 3 kinds of handlers success, failure, and catch. All handlers are optional, if you want to run task without handlers do it like this task.run({}). If a function passed as handlers it's automatically transformend to {success: fn}, so if you need only success handler you can do task.run(x => ...).

If failure handler isn't provided but task fails, an exception is thrown. You should always provided failure handlers for tasks that may fail. If you want to ignore failure pass a noop failure handler explicitly.

The catch handler is for errors thrown from functions passed to map, chain etc. More on how it works.

Task.of(2).run({
  success(x) {
    console.log(`result: ${x}`)
  },
  failure(error) {
    // handle failure ...
  },
  catch(error) {
    // handle error thrown from `map(fn)` etc ...
  },
})

// > result: 2

task.runAndLog()

Runs the task and prints results using console.log(). Mainly for testing / debugging etc.

Task.of(2).runAndLog()

// > Success: 2

task.toPromise([options])

Runs the task and returns a Promise that represent the result. The task's success and failure branches both correspond to the promise's success brach because the error branch in Promises is reserved for unexpected failures. The task's catch branch correspond to promise's error.

The promise's success value is either {success: s} or {failure: f} where s and f task's success or failure values.

If {catch: false} is passed as options the task is run without catch callback.

Task.of(2).toPromise().then(result => {
  if ('success' in result) {
    console.log(`success: ${result.success}`)
  } else {
    console.log(`failure: ${result.failure}`)
  }
})

// > success: 2

Task.fromPromise(promise)

Creates a Task from a Promise.

Task.fromPromise(Promise.resolve(2)).run({
  success(x) {
    console.log(`result: ${x}`)
  },
})

// result: 2

The promise argument must be either a Promise or a function that wnen called with no arguments returns a Promise. If a function is used as promise argument, that function is executed on each task's run to retrieve a new promise.

Task.fromPromise(() => Promise.resolve(2)).run({
  success(x) {
    console.log(`result: ${x}`)
  },
})

// result: 2

The promise's success corresponds to the task's success and promise's error corresponds to the task's catch.

Task.fromPromise(Promise.reject(2)).run({
  catch(x) {
    console.log(`error: ${x}`)
  },
})

// error: 2