Skip to content

Support traverse and sequence operations on more types #10934

@julienrf

Description

@julienrf

I’m copying the content of scala/collection-strawman#247 here.

We currently have the Future.traverse and Future.sequence methods that allow us to turn a Seq[Future[A]] into a Future[Seq[A]]. But I find these operations very useful on other types as well. For instance to turn:

  • an Option[Future[A]] into a Future[Option[A]],
  • an Either[E, Future[A]] into a Future[Either[E, A]],

but also to turn:

  • a List[Option[A]] into an Option[List[A]],
  • a List[Either[E, A]] into an Either[E, List[A]].
  • a List[Validated[A]] into a Validated[List[A]],

We can see that there are lots of possible combinations. To make the implementation concise and extensible (cf the example with an imaginary Validated data type which is not part of the stdlib) it’s probably a good idea to rely on typeclasses.

On one hand, a typeclass-based design (with Traverse and Applicative) would provide the highest level of reuse: once you define N instances of Traverse and M instances of Applicative you can combine them to handle N×M situations.

On the other hand, the discoverability of the traverse and sequence operations would be rather poor. Today it is easy to find Future.sequence (and to understand what it does) just by browsing the Scaladoc. But if, instead, we only find an instance of Applicative in Future’s documentation, it is not obvious what it can be useful for.

A compromise might be to have the typeclass-based design (so that advanced users have the full power) but also define specialized forwarders in companion objects of Applicative instances (such as Future).

In summary, the changes we would have to apply would be the following.

  1. Define Traverse and Applicative typeclasses
  2. Define useful instances
    • For Applicative, those that come to my mind are: Option, Future, Either
    • For Traverse: List, Option, Either
  3. Add forwarder methods in companion objects of Applicative instances. For instance, for Future:
    object Future {
      def traverse[F[_], A, B](fa: F[A])(f: A => Future[B])(implicit F: Traverse[F], ec: ExecutionContext): Future[F[B]] = F.traverse(fa)(f)
      def sequence[F[_], A](ffa: F[Future[A]])(implicit F: Traverse[F], ec: ExecutionContext): Future[F[A]] = F.sequence(ffa)
    }
    For the sake of comparison, the current definition of these methods is the following:
    object Future {
      def traverse[A, B, M[X] <: TraversableOnce[X]](in: M[A])(fn: A => Future[B])(implicit cbf: CanBuildFrom[M[A], B, M[B]], ec: ExecutionContext): Future[M[B]]
      def sequence[A, M[X] <: TraversableOnce[X]](in: M[Future[A]])(implicit cbf: CanBuildFrom[M[Future[A]], A, M[A]], ec: ExecutionContext): Future[M[A]]
    }
    I don’t think the one that uses Traverse is more complicated that the current one…

@Jasper-M pointed out that:

It feels a bit awkward to only add Traverse and Applicative just for this purpose, but not have all related typeclasses and operations.

And I agree with this point… I’d like to start a discussion on the topic. What do other users or scala maintainers think?

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions