Specifying an execution context for Monad[Future] when using EitherT in Scalaz 7

209 views Asked by At

I've been trying to tidy up some code which uses multiple functions that all return the type Future[Either[String, A]].

These functions don't compose neatly in a for comprehension because of the problem of having to peak inside the Future and then inside the Either to get the value. After using an EitherT monad transformer I found a solution I liked using EitherT, even though having to add eitherT and having to have the extra step of calling 'run' when you get the final result is not ideal.

My solution is below, but there's one thing I'm not happy with is that you need to create a Monad[Future] in order for eitherT to work, and this needs an execution context. There's no obvious way to do that. What I've done is to have an implicit execution context in scope of my code and create a Future Monad that I pass the same execution context to so that it both pieces of code use the same one. This seems a little messy, error prone.

Please let me know if there's a better way.

/*

Example of EitherT in ScalaZ

val scalaZVersion = "7.2.8"
 "org.scalaz" %% "scalaz-core" % scalaZVersion,
  "org.scalaz" %% "scalaz-effect" % scalaZVersion,
*/

import java.util.concurrent.Executors

import scala.concurrent.duration._
import org.scalatest._
import scala.concurrent.{Await, ExecutionContext, Future}
import scalaz.{-\/, Monad, \/, \/-}
import scalaz.EitherT.eitherT

object MonadFutureUtil {

  // a Future Monad with a specific instance of an EC
  case class MonadWithExecutionContext()(implicit ec : ExecutionContext) extends Monad[Future] {
    def point[A](a: => A): Future[A] = Future(a)
    def bind[A, B](fa: Future[A])(f: (A) => Future[B]): Future[B] = fa flatMap f
  }

}

class TestFutureUtil extends FlatSpec with Matchers with OptionValues with Inside with Inspectors {

  implicit val ec = new ExecutionContext {

    implicit val threadPool = Executors.newFixedThreadPool(8)

    def execute(runnable: Runnable) {
      threadPool.submit(runnable)
    }

    def reportFailure(t: Throwable): Unit = {
      println(s"oh no! ${t.getMessage}")

    }
  }

  implicit val monadicEC = MonadFutureUtil.MonadWithExecutionContext()(ec)

  // halves the input if it is even else fails
  def dummyFunction1(n: Int)(implicit ec : ExecutionContext) : Future[\/[String, Int]] = {
    Future.successful(
      if(n % 2 == 0)
        \/-(n / 2)
      else
        -\/("An odd number")
    )
  }

  // appends a suffix to the input after converting to a string
  // it doesn't like numbers divisible by 3 and 7 though
  def dummyFunction2(n: Int)(implicit ec : ExecutionContext) : Future[\/[String, String]] = {
    Future.successful(
      if(n % 3 != 0 && n % 7 != 0)
        \/-(n.toString + " lah!")
      else
        -\/(s"I don't like the number $n")
    )
  }

  "EitherFuture" should "add the results of two dummyFunction1 calls" in {

      val r = for (
        rb1 <- eitherT(dummyFunction1(8));
        rb2 <- eitherT(dummyFunction1(12))

      ) yield (rb1 + rb2)

      r.run.map {
        _ shouldBe \/-(11)
      }
  }

  it should "handle functions with different type" in {

    val r = for (
      rb1 <- eitherT(dummyFunction1(14));
      rb2 <- eitherT(dummyFunction1(12));
      rb3 <- eitherT(dummyFunction2(rb2 + rb1))

    ) yield rb3

    val r2 = Await.result(r.run.map {
      case \/-(s) =>
        (s == "13 lah!")
      case -\/(e) =>
        false
    }, 5 seconds)

    assert(r2)
  }

  it should "doesn't like divisible by 7" in {

    val r = for (
      rb1 <- eitherT(dummyFunction1(14));
      rb2 <- eitherT(dummyFunction1(14));
      rb3 <- eitherT(dummyFunction2(rb1 + rb2))

    ) yield rb3

    val r2 = Await.result(r.run.map {
      case \/-(s) =>
        false
      case -\/(e) =>
        true
    }, 5 seconds)

    assert(r2)
  }

}
1

There are 1 answers

0
OlivierBlanvillain On BEST ANSWER

I would suggest trying the following instead of a case class:

implicit def MWEC(implicit ec: ExecutionContext): Monad[Future] = ???

This way it should be harder to mix up execution contexts. The proper way would be to use a pure IO abstraction, one that doesn't require a execution context to be mapped/flatmapped over...