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

Task.cancel as a pure action with .start, .race and .uncancelable #494

merged 6 commits into from Jan 7, 2018


None yet
1 participant

alexandru commented Jan 7, 2018

New additions to Task, making its API purer and more powerful out of the box:

sealed abstract class Task[+A]
  // ...
  def cancel: Task[Unit]

  def start: Task[Task[A]]

  def uncancelable: Task[A]

object Task {
  // ...
  def race[A, B](fa: Task[A], fb: Task[B]): Task[Either[A, B]] = ???

  def raceMany[A](tasks: TraversableOnce[Task[A]]): Task[A] = ???

  def racePair[A,B](fa: Task[A], fb: Task[B]): Task[Either[(A, Task[B]), (Task[A], B)]] = ???

NOTE — no changes to the run-loop or internal encoding have been made, these are simply operations that were added on top of the existing implementation, but needed nevertheless for a good out of the box experience.

In the future changes to the internal encoding might happen. For example at this point there's no special state that describes cancellation, the result of .cancel being a Task.Async that makes use of the existing cancellation protocol. But in the future we might add a special state for it, depending on feedback.

Pure cancellation

It is now possible to do this:

val ta: Task[A] = ???
val tb: Task[B] = ???

Task.racePair(ta, tb).flatMap {
  case Left((a, taskB)) => => a)
  case Right((taskA, b)) => => b)

You might recognize this operation as the old Task.chooseFirstOf, however the old signature was returning CancelableFuture as the reference for the future result of the losing task, whereas this one returns a pure Task (linked to an underlying Promise, but that's an implementation detail). And because now Task has a pure .cancel operation that describes cancellation, you can cancel these tasks in racePair without side effects being involved.

val task = Task.eval(println("Hello!"))

//=> Cancelled!


Inspired by FS2, we can now trigger start as a pure operation, allowing operations like this:

def par2[A, B](ta: Task[A], tb: Task[B]): Task[(A, B)] =
  for {
    fa <- ta.start
    fb <- tb.start
     a <- fa
     b <- fb
  } yield (a, b)

Note this is not exactly equivalent with parMap2 due to insufficient behavior when an error happens. To make this behave like parMap2 is harder, so usage of parMap2 (and parMap3, etc) is still recommended, this being just a pedagogical example for what start might be useful for.

Also compared with the equivalent from FS2, Monix's start does not introduce an automatic fork (i.e. it does not shift the processing on another thread, although shifting might happen due to batched processing), being consistent with other operations in Monix.


Sometimes we need to ensure that tasks are not cancelable, in order to ensure that some actions are executed no matter what and now you can use the new uncancelable operator to mark tasks that cannot be canceled:

  .doOnFinish(_ => println("Executed!"))

Normally the above task is cancelable and there's plenty of time to trigger cancellation (due to the delayExecution), so if we want to ensure that this task executes as an atomic unit (all or nothing), we can call .uncancelable on it.

race, racePair, raceMany

The old .chooseFirstOf was too powerful. Most of the time you just want to pick the winner and cancel the loser. Plus race as a name is much more standard, having been used both in Haskell and JavaScript's standard libraries at least.

race is the new version that automatically cancels the loser.

racePair is the new version that behaves like the old chooseFirstOf, but is pure, giving you a Task for the loser, since tasks can now be cancelled directly as a pure action.

raceMany is just the old chooseFirstOfList, to be consistent with the other two.

Other Changes

Refactoring to monix.execution.cancelables:

  • rename MultiAssignmentCancelable to OrderedCancelable
  • introduce new MultiAssignCancelable with a simpler implementation
  • rename SingleAssignmentCancelable to SingleAssignCancelable
  • add deprecated symbols in cancelable package object

This comment has been minimized.

codecov bot commented Jan 7, 2018

Codecov Report

Merging #494 into master will increase coverage by 0.03%.
The diff coverage is 94.01%.

@@            Coverage Diff             @@
##           master     #494      +/-   ##
+ Coverage   89.91%   89.95%   +0.03%     
  Files         352      356       +4     
  Lines        9053     9127      +74     
  Branches     1776     1785       +9     
+ Hits         8140     8210      +70     
- Misses        913      917       +4

alexandru added some commits Jan 7, 2018

@alexandru alexandru changed the title from Refactor MultiAssignmentCancelable to Add Task.cancel as a pure action, along with Task.start and Task.race Jan 7, 2018

@alexandru alexandru changed the title from Add Task.cancel as a pure action, along with Task.start and Task.race to Task.cancel as a pure action with Task.start and Task.race Jan 7, 2018

@alexandru alexandru changed the title from Task.cancel as a pure action with Task.start and Task.race to Task.cancel as a pure action with .start, .race and .uncancelable Jan 7, 2018


This comment has been minimized.


alexandru commented Jan 7, 2018

Merging, so I can release 3.0.0-M3, will address potential issues later.

@alexandru alexandru merged commit 5e3ebb8 into monix:master Jan 7, 2018

1 check passed

continuous-integration/travis-ci/pr The Travis CI build passed

@alexandru alexandru referenced this pull request Jan 12, 2018


Add Bracket type class #113

@alexandru alexandru added this to the 3.0.0 milestone Jan 21, 2018

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment