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.
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
I assume you mean the fact that you now have an Option[Try[Post]]
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")
}
Could be done as a for a few ways.
Nesting fors:
for {
post <- find(id)
} yield {
for {
tryOfPost <- permitted(post, userId)
} yield {
tryOfPost.copy(text = text)
}
}
Convert Option to a Try so you're using a single type, this has the disadvantage of losing the difference between an error in the Try and a None from the Option. credit here for how to go from Option to Try.
for {
post <- find(id).fold[Try[Post]](Failure[Post](new OtherException))(Success(_))
permittedPost <- permitted(post, userId)
} yield {
permittedPost.copy(text = text)
}
You could also look into the OptionT monad transformer in scalaz to create a type which is an OptionTTry.
Fundamentally, though, Monads don't compose this way, at least not generically.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With