Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to handle `Reader` monad and `Try`?

I'm reading this great article about dependency injection in scala with Reader monad.

The original example is working well, but I did a little bit change on the return types of the UserRepository.get/find. It was User, but I changed it to Try[User].

Then the code won't be compiled, I had tries many times, but still without lucky.

import scala.util.Try
import scalaz.Reader

case class User(email: String, supervisorId: Int, firstName: String, lastName: String)

trait UserRepository {
  def get(id: Int): Try[User]

  def find(username: String): Try[User]
}

trait Users {

  def getUser(id: Int) = Reader((userRepository: UserRepository) =>
    userRepository.get(id)
  )

  def findUser(username: String) = Reader((userRepository: UserRepository) =>
    userRepository.find(username)
  )
}

object UserInfo extends Users {

  def userEmail(id: Int) = {
    getUser(id) map (ut => ut.map(_.email))
  }

  def userInfo(username: String) =
    for {
      userTry <- findUser(username)
      user <- userTry     // !!!!!!!! compilation error
      bossTry <- getUser(user.supervisorId)
      boss <- bossTry     // !!!!!!!! compilation error
    } yield Map(
      "fullName" -> s"${user.firstName} ${user.lastName}",
      "email" -> s"${user.email}",
      "boss" -> s"${boss.firstName} ${boss.lastName}"
    )
}

The compilation error is:

Error:(34, 12) type mismatch;
 found   : scala.util.Try[Nothing]
 required: scalaz.Kleisli[scalaz.Id.Id,?,?]
      user <- userTry
           ^

and

Error:(36, 12) type mismatch;
 found   : scala.util.Try[scala.collection.immutable.Map[String,String]]
 required: scalaz.Kleisli[scalaz.Id.Id,?,?]
      boss <- bossTry
           ^

I read the document of Kleisli.flatMap (The return type of findUser and getUser is Kleisli), it requires the parameter type is:

B => Kleisli[M, A, C]

Since a Try won't be a Kleisli, there are such errors.

I'm not sure how to handle it. Can I use scala.util.Try here? How can I turn it to a KLeisli type? How can I make this example work?

like image 930
Freewind Avatar asked Sep 20 '14 14:09

Freewind


People also ask

How does the reader monad work?

The Reader monad, or more generally the MonadReader interface, solves the problem of threading the same configuration to many functions. load :: Config -> String -> IO Stringload config x = readFile (config ++ x)

What is the Writer monad?

The Writer monad provides us with an easier way to track this value. It would also make it easier for us to represent the cost with a different type. But to understand how, we should first learn two typeclasses, Semigroup and Monoid, that help us generalize accumulation. A Semigroup is any type that we accumulate, via an "append" operation.

How to run a monad from a pure context?

Whenever you learn about a monad "X", there's often a corresponding function "runX" that tells you how to run operations of that monad from a pure context (IO is an exception). This function will often require some kind of input, as well as the computation itself. Then it will produce the final output of the computation.

How do you bind a monad action to a function?

The monad's bind action allows us to glue different Reader actions together together. In order to call a reader action from pure code, all we need to do is call the runReader function and supply the environment as a parameter. All functions within the action will be able to treat it like a global variable.


1 Answers

You can use the ReaderT monad transformer to compose the Reader monad and the Try monad into a single monad that you can use a for-comprehension on, etc.

ReaderT is just a type alias for Kleisli, and you can use Kleisli.kleisli instead of Reader.apply to construct your Reader-y computations. Note that you need scalaz-contrib for the monad instance for Try (or you can write your own—it's pretty straightforward).

import scala.util.Try
import scalaz._, Scalaz._
import scalaz.contrib.std.utilTry._

case class User(
  email: String,
  supervisorId: Int,
  firstName: String,
  lastName: String
)

trait UserRepository {
  def get(id: Int): Try[User]

  def find(username: String): Try[User]
}

trait Users {
  def getUser(id: Int): ReaderT[Try, UserRepository, User] =
    Kleisli.kleisli(_.get(id))

  def findUser(username: String): ReaderT[Try, UserRepository, User] =
    Kleisli.kleisli(_.find(username))
}

Now that that's done, UserInfo is much simpler (and it compiles now, too!):

object UserInfo extends Users {
  def userEmail(id: Int) = getUser(id).map(_.email)

  def userInfo(
    username: String
  ): ReaderT[Try, UserRepository, Map[String, String]] =
    for {
      user <- findUser(username)
      boss <- getUser(user.supervisorId)
    } yield Map(
      "fullName" -> s"${user.firstName} ${user.lastName}",
      "email" -> s"${user.email}",
      "boss" -> s"${boss.firstName} ${boss.lastName}"
    )
}

We can show it works:

import scala.util.{ Failure, Success }

val repo = new UserRepository {
  val bar = User("[email protected]", 0, "Bar", "McFoo")
  val foo = User("[email protected]", 0, "Foo", "McBar")

  def get(id: Int) = id match {
    case 0 => Success(bar)
    case 1 => Success(foo)
    case i => Failure(new Exception(s"No user with id $i"))
  }

  def find(username: String) = username match {
    case "bar" => Success(bar)
    case "foo" => Success(foo)
    case other => Failure(new Exception(s"No user with name $other"))
  }
}

And then:

UserInfo.userInfo("foo").run(repo).foreach(println)
Map(fullName -> Foo McBar, email -> [email protected], boss -> Bar McFoo)

Exactly the same way you'd run a Reader, but you get a Try at the end.

like image 126
Travis Brown Avatar answered Oct 03 '22 14:10

Travis Brown