synapse synapse - 3 months ago 20
Scala Question

Using different monads in for-comprehension

Can different monads be used in for-comprehensions? Here's the code that uses

map


case class Post(id: Int, text: String)

object PostOps {
def find(id: Int) : Option[Post] = if (id == 1) Some(Post(1, "text")) else None

def permitted(post: Post, userId: Int) : Try[Post] = if (userId == 1) Success(post) else Failure(new UnsupportedOperationException)

def edit(id: Int, userId : Int, text: String) = find(id).map(permitted(_, userId).map(_.copy(text = text))) match {
case None => println("Not found")
case Some(Success(p)) => println("Success")
case Some(Failure(_)) => println("Not authorized")
}
}


The straightforward version of for-comprehension doesn't work for obvious reasons, but is it possible to make it work with some additional code? I know it's possible in C# so it would be weird if it is not in Scala.

Answer

You can only use one type of monad in a for comprehension, since it is just syntactic sugar for flatMap and map.

If you have a stack of monads (eg Future[Option[A]]) you could use a monad transformer, but that does not apply here.

A solution for your case could be to use one monad : go from Option to Try or go from both Option and Try to Either[String, A].

def tryToEither[L, R](t: Try[R])(left: Throwable => L): Either[L, R] = 
  t.transform(r => Success(Right(r)), th => Success(Left(left(th)))).get

def edit(id: Int, userId: Int, text: String) = {
  val updatedPost = for {
    p1 <- find(id).toRight("Not found").right
    p2 <- tryToEither(permitted(p1, userId))(_ => "Not Authorized").right
  } yield p2.copy(text = text)
  updatedPost match {
    case Left(msg) => println(msg)
    case Right(_)  => println("success")
  }
}

You could define an error type instead of using String, this way you can use Either[Error, A].

sealed trait Error extends Exception
case class PostNotFound(userId: Int) extends Error
case object NotAuthorized extends Error