Jak poradzić sobie z monadą "czytelnika" i "spróbować"?

Czytam świetny artykuł o dependency injection w Scali z Reader monad.

Oryginalny przykład działa dobrze, ale trochę zmieniłem typy zwrotów UserRepository.get/find. To było User, ale zmieniłem na Try[User].

Wtedy kod nie zostanie skompilowany, próbowałem wiele razy, ale nadal bez szczęścia.

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}"
    )
}

Błąd kompilacji to:

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

I

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 z findUser i getUser jest Kleisli), wymaga typu parametru:

B => Kleisli[M, A, C]

Ponieważ Try nie będzie Kleisli, są takie błędy.

Nie wiem, jak sobie z tym poradzić. Czy mogę użyć scala.util.Try tutaj? Jak Mogę zmienić go na typ KLeisli? Jak sprawić, by ten przykład działał?
Author: Freewind, 2014-09-20

1 answers

Możesz użyć transformatora ReaderT monad, aby skomponować monadę Reader i monadę Try w jedną monadę, na której możesz użyć for-zrozumienie itp.

ReaderT jest tylko aliasem typu dla Kleisli i możesz użyć Kleisli.kleisli zamiast Reader.apply do konstruowania obliczeń Reader-y. Pamiętaj, że potrzebujesz scalaz-contrib dla instancji monad dla Try (lub możesz napisać własną-to dość proste).

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))
}

Teraz, gdy to się stało, UserInfo jest znacznie prostsze (i to teraz też kompiluje!):

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}"
    )
}

Możemy pokazać, że działa:

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"))
  }
}

A potem:

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

Dokładnie w ten sam sposób, w jaki uruchomiłeś Reader, ale dostajesz Try na końcu.

 20
Author: Travis Brown,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-09-20 17:12:55