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?
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)
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.
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.
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.
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.
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