6

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.

Artur Nowak
  • 5,254
  • 3
  • 22
  • 32
synapse
  • 5,588
  • 6
  • 35
  • 65
  • Monads don't generally compose, but Functors (and Applicatives) always do; therefore, since you're just using `map` it should be possible to do something along the lines of what you want. But it seems you're going to need to somehow specify which Functor you want to use in the `map`, i.e. `Option[_]` or `Option[Try[_]]`. – Jason Scott Lenderman Oct 28 '15 at 21:20

2 Answers2

5

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 
Peter Neyens
  • 9,770
  • 27
  • 33
  • Ultimately I'll need to throw futures in the mix (`find` will return `Future[Option[_]]`), could you elaborate on monad transformers? – synapse Oct 29 '15 at 00:47
  • Two articles about monad transformers in Scala which helped me: one by [Raul Raja](http://www.47deg.com/blog/fp-for-the-average-joe-part-2-scalaz-monad-transformers) and one by [Noel Welsh](http://underscore.io/blog/posts/2013/12/20/scalaz-monad-transformers.html) – Peter Neyens Oct 29 '15 at 07:58
3

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.

Community
  • 1
  • 1
Angelo Genovese
  • 3,398
  • 17
  • 23