2

Given a method that return a Kleisli given a parameter

def k[A, B, C](a: A) : Kleisli[Future, B, Option[C]] = ???

I want to write a combinator that deals with a sequence of this parameter

def ks[A, B, C](as: Seq[A]) : Kleisli[Future, B, Seq[C]] = Kleisli[Future, B, Seq[C]] {
  ctx => Future.sequence(as.map(a => k(a).run(ctx))).map(_.flatten)
}

Is there a better way ?

Yann Moisan
  • 8,161
  • 8
  • 47
  • 91

1 Answers1

3

There is a better way:

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scalaz._, Scalaz._

def k[A, B, C](a: A): Kleisli[Future, B, Option[C]] = ???

def ks[A, B, C](as: List[A]): Kleisli[Future, B, List[C]] =
  as.traverseU(k[A, B, C]).map(_.flatten)

Note that I'm using List instead of Seq since Scalaz doesn't provide a Traverse instance for Seq (see my answer here for some discussion of why it doesn't).

This is one of the big advantages of using Kleisli in the first place—if F has an Applicative instance, then so does Kleisli[F, In, ?] for any In. In this case that means that you can use traverse instead of manually sequencing with map and Future.sequence.


Update: want to get super-fancy here? Probably not, but just in case, you could actually take the abstraction one last step and move the context of the return type into the context of the Kleisli:

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scalaz._, Scalaz._

type FutureOption[x] = OptionT[Future, x]
type FutureList[x] = ListT[Future, x]

def k[A, B, C](a: A): Kleisli[FutureOption, B, C] = ???

def ks[A, B, C](as: List[A]): Kleisli[FutureList, B, C] =
  Kleisli.kleisliMonadTrans[B].liftMU(
    ListT.fromList(as.point[Future])
  ).flatMap(k[A, B, C](_).mapK[FutureList, C](_.toListT))

This allows us to map, etc. directly over the result type while ignoring the fact that we're getting that result in a list in a future after applying the Kleisli to an input value.

Concretely:

import scala.util.Try

def k(s: String): Kleisli[FutureOption, Int, Int] = Kleisli[FutureOption, Int, Int] { in =>
  OptionT(Future(Try(s.toInt + in).toOption))
}

def ks(as: List[String]): Kleisli[FutureList, Int, Int] =
  Kleisli.kleisliMonadTrans[Int].liftMU(
    ListT.fromList(as.point[Future])
  ).flatMap(k(_).mapK[FutureList, Int](_.toListT))

And then:

import scala.concurrent.Await
import scala.concurrent.duration._

scala> import scala.concurrent.Await
import scala.concurrent.Await

scala> import scala.concurrent.duration._
import scala.concurrent.duration._

scala> Await.result(ks(List("1", "2", "a", "3")).run(0).run, 1.second)
res0: List[Int] = List(1, 2, 3)

And the punchline:

scala> val mapped = ks(List("1", "2", "a", "3")).map(_ + 1)
mapped: scalaz.Kleisli[FutureList,Int,Int] = Kleisli(<function1>)

scala> Await.result(mapped.run(0).run, 1.second)
res1: List[Int] = List(2, 3, 4)

Should you actually do this? Again, probably not, but it works, and it is kind of cool to be able to map way into a complex computation like this.

Community
  • 1
  • 1
Travis Brown
  • 138,631
  • 12
  • 375
  • 680
  • Thanks. I was stuck with `as.traverseU(k)` and this compilation error Error:(40, 24) not enough arguments for method traverseU: (implicit G: scalaz.Unapply[scalaz.Applicative,scalaz.Kleisli[scala.concurrent.Future,Nothing,Option[Nothing]]])G.M[List[G.A]]. Unspecified value parameter G. as.toList.traverseU(k).map(_.flatten) ^ – Yann Moisan Apr 08 '16 at 07:50