When inserting a value into a persistence layer and returning the result object it is usually a good practice to fetch the newly created entity instead of returning the input data again.
When I try to do this in Scala using reactivemongo I stumble over my language skills.
def create(user: User): Future[User] = {
val newUser = user.createOID()
collection.insert(newUser).map {
case ok if ok.ok => {
for {
createdUser <- this.findOne(BSONDocument("_id" -> newUser._id))
} yield {
createdUser match {
case None => throw new RuntimeException("Could not find just created user")
case Some(x) => x
}
}
}
case error => throw new RuntimeException(error.message)
}
}
Where findOne has the signature:
def findOne(query: BSONDocument): Future[Option[User]]
I get the following error:
[error] found : scala.concurrent.Future[models.User]
[error] required: models.User
[error] createdUser <- this.findOne(BSONDocument("_id" -> newUser._id))
[error] ^
If I return the newUser object everything is fine.
I think I have a general misunderstanding what is happening here - maybe there is a better way to fetch the created object in one shot.
I would say that idiomatic Play/Scala way to do that is the following
def create(user: User): Future[Option[User]] = {
val newUser = user.createOID()
for {
nu <- collection.insert(newUser)
createdUser <- findOne(BSONDocument("_id" -> newUser._id))
} yield {
createdUser
}
}
Notice that this does return Future[Option[User]] and not Future[User] as in your code. I believe that Option[User] is definitely the way to go in this case as it actually tells clients of this method that it's not guaranteed that insertion will succeed (and thus runtime exception is not required as client will do .map on the result of this method — avoid using exceptions if you can deal with them gracefully).
You might also check nu for being ok within yield.
Related
My am making 3 database queries, each return a Future. I am trying to use for comprehension to resolve the Futures but it seems I am not using if correctly in for
Each query depends on result of previous one. I look for a token, if found, I look for user and it found, I update the user. Each database query returns a Future[Option]] and I thought I could considitionally perform the next query depending on whether the previous one returns Some or None. I am using isDefined for this. But when I ran the code for an invalid token, I got error [NoSuchElementException: None.get] for code userOption:Option[User]<-userRepo.findUser(tokenOption.get.loginInfo); if tokenOption.isDefined
def verifyUser(token:String) = Action.async {
implicit request => {
val result:Future[Result] = for{
//generator 1 - get token from database
tokenOption:Option[UserToken] <- userTokenRepo.find(UserTokenKey(UUID.fromString(token)))
//generator2. found token, look for corresponding user to which the token belongs
userOption:Option[User] <- userRepo.findUser(tokenOption.get.loginInfo); if tokenOption.isDefined
//generator 3. found user and token. Update profile
modifiedUser:Option[User] <- confirmSignupforUser(userOption.get); if userOption.isDefined
} yield
{ //check if we have user and token and modified user here. If any is missing, return error else success
if(tokenOption.isDefined && userOption.isDefined && modifiedUser.isDefined)
Redirect("http://localhost:9000/home"+";signup=success")//TODOM - pick from config
else
if(tokenOption.isEmpty)
Redirect("http://localhost:9000/home"+";signup=error")//TODOM - pick from config
else if(userOption.isEmpty)
Redirect("http://localhost:9000/home"+";signup=error")//TODOM - pick from config
else if(modifiedUser.isEmpty)
Redirect("http://localhost:9000/home"+";signup=error")//TODOM - pick from config
else //this shouldn't happen. Unexpected
Redirect("http://localhost:9000/home"+";signup=error")//TODOM - pick from config
}
result
}
}
TL;DR
Consider using OptionT
https://typelevel.org/cats/datatypes/optiont.html
Have a look at my toned down implementation:
from https://scastie.scala-lang.org/hsXXtRAFRrGpMO1Jl1Li7A
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Await.result
import scala.concurrent.duration._
import scala.language.postfixOps
type UserToken = String
type User = String
def fromToken(token: String): Future[Option[UserToken]] = Future.successful(None)
def findUser(userToken: UserToken): Future[Option[User]] = Future.successful(None)
def modify(user: User): Future[Option[User]] = Future.successful(None)
def verifyUser(token: String) = {
val result = for {
tokenOption: Option[UserToken] <- fromToken(token) //generator 1 - get token from database
userOption: Option[User] <- findUser(tokenOption.get);
if tokenOption.isDefined //generator2. found token, look for corresponding user to which the token belongs
modifiedUser: Option[User] <- modify(userOption.get);
if userOption.isDefined //generator 3. found user and token. Update profile
} yield { //check if we have user and token and modified user here. If any is missing, return error else success
if (tokenOption.isDefined && userOption.isDefined && modifiedUser.isDefined)
println("happy")
else
println("sad")
}
result
}
result(verifyUser("hello"), 1 second)
I used the following compile time flags, the last one is important:
scalacOptions ++= Seq(
"-deprecation",
"-encoding", "UTF-8",
"-feature",
"-unchecked",
"-Xprint:typer"
)
Let's focus on this line of the compile output:
(((tokenOption: Option[Playground.this.UserToken]) => Playground.this.findUser(tokenOption.get).
withFilter(((check$ifrefutable$2: Option[Playground.this.User]) => (check$ifrefutable$2: Option[Playground.this.User] #unchecked) match {
case (userOption # (_: Option[Playground.this.User])) => true
case _ => false
...
You can see that the tokenOption.get is invoked before the withFilter. These gets are the source of the exception you get
The, almost complete output of the compile is:
[[syntax trees at end of typer]] // main.scala
....
import scala.concurrent.Future;
import scala.concurrent.ExecutionContext.Implicits.global;
import scala.concurrent.Await.result;
import scala.concurrent.duration._;
import scala.language.postfixOps;
type UserToken = String;
type User = String;
def fromToken(token: String): scala.concurrent.Future[Option[Playground.this.UserToken]] = scala.concurrent.Future.successful[None.type](scala.None);
def findUser(userToken: Playground.this.UserToken): scala.concurrent.Future[Option[Playground.this.User]] = scala.concurrent.Future.successful[None.type](scala.None);
def modify(user: Playground.this.User): scala.concurrent.Future[Option[Playground.this.User]] = scala.concurrent.Future.successful[None.type](scala.None);
def verifyUser(token: String): scala.concurrent.Future[Unit] = {
val result: scala.concurrent.Future[Unit] = Playground.this.fromToken(token).withFilter(((check$ifrefutable$1: Option[Playground.this.UserToken]) => (check$ifrefutable$1: Option[Playground.this.UserToken] #unchecked) match {
case (tokenOption # (_: Option[Playground.this.UserToken])) => true
case _ => false
}))(scala.concurrent.ExecutionContext.Implicits.global).flatMap[Unit](((tokenOption: Option[Playground.this.UserToken]) => Playground.this.findUser(tokenOption.get).withFilter(((check$ifrefutable$2: Option[Playground.this.User]) => (check$ifrefutable$2: Option[Playground.this.User] #unchecked) match {
case (userOption # (_: Option[Playground.this.User])) => true
case _ => false
}))(scala.concurrent.ExecutionContext.Implicits.global).withFilter(((userOption: Option[Playground.this.User]) => tokenOption.isDefined))(scala.concurrent.ExecutionContext.Implicits.global).flatMap[Unit](((userOption: Option[Playground.this.User]) => Playground.this.modify(userOption.get).withFilter(((check$ifrefutable$3: Option[Playground.this.User]) => (check$ifrefutable$3: Option[Playground.this.User] #unchecked) match {
case (modifiedUser # (_: Option[Playground.this.User])) => true
case _ => false
}))(scala.concurrent.ExecutionContext.Implicits.global).withFilter(((modifiedUser: Option[Playground.this.User]) => userOption.isDefined))(scala.concurrent.ExecutionContext.Implicits.global).map[Unit](((modifiedUser: Option[Playground.this.User]) => if (tokenOption.isDefined.&&(userOption.isDefined).&&(modifiedUser.isDefined))
scala.Predef.println("happy")
else
scala.Predef.println("sad")))(scala.concurrent.ExecutionContext.Implicits.global)))(scala.concurrent.ExecutionContext.Implicits.global)))(scala.concurrent.ExecutionContext.Implicits.global);
result
};
scala.Predef.locally[Unit]({
val $t: Unit = scala.concurrent.Await.result[Unit](Playground.this.verifyUser("hello"), scala.concurrent.duration.`package`.DurationInt(1).second);
Playground.this.instrumentationMap$.update(com.olegych.scastie.api.Position.apply(1199, 1236), com.olegych.scastie.api.runtime.Runtime.render[Unit]($t)((ClassTag.Unit: scala.reflect.ClassTag[Unit])));
$t
})
};
object Main extends scala.AnyRef {
def <init>(): Main.type = {
Main.super.<init>();
()
};
private[this] val playground: Playground = new Playground();
<stable> <accessor> def playground: Playground = Main.this.playground;
def main(args: Array[String]): Unit = scala.Predef.println(com.olegych.scastie.api.runtime.Runtime.write(Main.this.playground.instrumentations$))
}
}
I am not sure why you are surprised you are getting and error for None.get with invalid token: if token is invalid, tokenOption is None, so, the next statement tokenOption.get will fail with exactly this error.
You want the "guard" executed before the statement you want to short circuit, not after it:
for {
foo <- bar if foo.isDefined
baz <- foo.get
} yield baz
But this would fail in the end anyway, because there would be nothing to yield (this trick works with Options or Lists etc., but Future.withFilter will end up failing if predicate is not satisfied, there is no other alternative).
The general rule to avoid this kind of errors is never use .get on an Option (or on a Try). Also, never use .head on a List, .apply on a Map, etc.
Here is one (almost) idiomatic way to write what you want:
case object Error extends RuntimeException("")
userTokenRepo
.find(UserTokenKey(UUID.fromString(token)))
.map { _.getOrElse(throw Error)
.flatMap { userRepo.find(_.loginInfo) }
.map { _.getOrElse(throw Error) }
.flatMap(confirmSignupForUser)
.map { _.getOrElse(throw Error) }
.map { _ => "success") }
.recover { case Error => "error" }
.map { result => Redirect(s"http://localhost:9000/home;signup=$result" }
Note, I said this was "almost" idiomatic, because throwing exceptions in scala is frowned upon. A purist would object to it, and suggest using something like a Try . or a biased Either instead, or to make use of a third party library, like cats or scalaz, that provide additional tools for working with Futures of Option (namely, OptionT).
But I would not recommend getting into that right now. You should get comfortable enough with basic "vanilla" scala before starting with that advanced stuff to avoid ending up with something completely incomprehensible.
You could also write this differently, in a completely idiomatic way (without using exceptions), with something like this:
userTokenRepo.find(UserTokenKey(UUID.fromString(token)))
.flatMap {
case Some(token) => userRepo.find(token.loginInfo)
case None => Future.successful(None)
}.flatMap {
case Some(user) => confirmSignupForUser(user)
case None => Future.successful(None)
}.map {
case Some(_) => "success"
case None => "error"
}.map { result =>
Redirect(s"http://localhost:9000/home;signup=$result"
}
This is more "pure", but a little more repetitive, so my personal preference is the first variant.
Finally, you could do away with my Error thingy, and just handle the NoSuchElement exception directly. This is going to be the shortest, but kinda icky even to my taste (what if some downstream code throws this exception because of a bug?):
userTokenRepo
.find(UserTokenKey(UUID.fromString(token)))
.flatMap { userRepo.find(_.get.loginInfo) }
.flatMap(confirmSignupForUser(_.get))
.map { _.get }
.map { _ => "success") }
.recover { case _: NoSuchElementException => "error" }
.map { result =>
Redirect(s"http://localhost:9000/home;signup=$result"
}
I really don't recommend the last version though, despite it being the shortest, and arguably, the most readable one (you can even rewrite it with a for-comprehension to look even nicer). Using Option.get is commonly considered "code smell", and is almost never a good thing to do.
Motivated by How to best handle Future.filter predicate is not satisfied type errors
I rewrote like the following. While the code works, I am curious to know if I am doing it the right way (functional!). Does it look fine?
def verifyUser(token:String) = Action.async {
implicit request => {
println("verifyUser action called with token: " + token) //TODOM - add proper handling and response
val result:Future[Result] = for{tokenOption:Option[UserToken] <- userTokenRepo.find(UserTokenKey(UUID.fromString(token))) //generator 1 - get token from database
userOption:Option[User] <- if (tokenOption.isDefined) userRepo.findUser(tokenOption.get.loginInfo) else Future.successful(None) //generator2. found token, look for corresponding user to which the token belongs
modifiedUser:Option[User] <- if (userOption.isDefined) confirmSignupforUser(userOption.get) else Future.successful(None) //generator 3. found user and token. Update profile
deletedToken:Option[UserTokenKey] <- if(modifiedUser.isDefined) userTokenRepo.remove(UserTokenKey(UUID.fromString(token))) else Future.successful(None)
}
yield { //check if we have user and token and modified user here. If any is missing, return error else success
println("db query results tokenOption: "+tokenOption+", userOption: "+userOption+" : modifiedUserOption: "+modifiedUser+", deletedToken: "+deletedToken)
if(tokenOption.isDefined && userOption.isDefined && modifiedUser.isDefined && deletedToken.isDefined)
Redirect("http://localhost:9000/home"+";signup=success")//TODOM - pick from config
else
if(tokenOption.isEmpty)
Redirect("http://localhost:9000/home"+";signup=error")//TODOM - pick from config
else if(userOption.isEmpty)
Redirect("http://localhost:9000/home"+";signup=error")//TODOM - pick from config
else if(modifiedUser.isEmpty)
Redirect("http://localhost:9000/home"+";signup=error")//TODOM - pick from config
else //this shouldn't happen. Unexpected
Redirect("http://localhost:9000/home"+";signup=error")//TODOM - pick from config
}
result
}
}
The following block of code fails to build with error :
value flatMap is not a member of Product with Serializable
[error] if (matchingUser.isDefined) {
Here's the code:
for {
matchingUser <- userDao.findOneByEmail(email)
user <- {
if (matchingUser.isDefined) {
matchingUser.map(u => {
// update u with new values...
userDao.save(u)
u
})
} else {
val newUser = new User(email)
userDao.create(newUser)
newUser
}
}
} yield user
Method userDao.findOneByEmail(email) returns anFuture[Option[User]]object.
My Google searches are only aboutEitherwithRightandLeft` types.
Maybe I'm not doing this the proper way, please teach me how to properly do this.
The first branch of the if statement returns Option[User], the other one returns User. So, the result of the entire statement is inferred to have type Product with Serializable because it is the only common supertype of the two.
You could wrap the last statement inside the if into an Option (just do Option(newUser) instead of newUser) or, better yet, use fold instead of the whole if(matchingUser.isDefined) {...} thingy:
matchingUser.fold {
val u = new User(email)
userDao.create(u)
u
} { u =>
userDao.save(u)
u
}
This will make the result of that statement to be Option[User] as you probably intended ... but it still will not compile.
The problem is that you cannot mix different types of monads in the for-comprehension: since the first one was Future, all the others have to be as well. You can't have an Option there.
How to get around that? Well, one possibility is to make userDao.create and userDao.save return a future of the object they just saved. That is, probably a better thing to do in general, then what you have, because now you are returning the user before it was actually stored ... What if the create operation fails afterwards? Then you can just rewrite your for-comprehension like this:
for {
matchingUser <- userDao.findOneByEmail(email)
user <- matchingUser.fold(userDao.create(new User(email)))(userDao.save)
} yield user
Or just get rid of it entirely (for-comprehension is an overkill for simple cases like this):
userDao
.findOneByEmail(email)
.flatMap(_.fold(usrDao.create(new User(email)))(userDao.save))
Or, it may look a little nicer with pattern matching instead of fold in this case:
userDao
.findOneByEmail(email)
.flatMap {
case Some(u) => userDao.save(u)
case None => userDao.create(new User(email))
}
Here is my test example to reproduce your issue with solution.
Basically your user returns wrapper around Future (Option of Future), but it expected to be Future, as the first statement in for-comprehension.
That is why I've applied some addition unwrapping. See sample below.
Note: it does not looks so nice, I'd prefer to rewrite it with flatMap map.
object T {
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
def future1: Future[Option[Int]] = ???
def future2(i: Int): Future[Double] = ???
for {
matchingUser <- future1
user <- {
if (matchingUser.isDefined) {
matchingUser.map { i =>
future2(i)
}
} else {
Some(future2(42))
}
} match {
case None => Future.successful(-42.0)
case Some(x) => x
}
} yield user
}
And the same implemented with flatMap:
val userFuture = future1.flatMap {
case Some(i) => future2(i)
case None => future2(42)
}
I'm again seeking you to share your wisdom with me, the scala padawan!
I'm playing with reactive mongo in scala and while I was writting a test using scalatest, I faced the following issue.
First the code:
"delete" when {
"passing an existent id" should {
"succeed" in {
val testRecord = TestRecord(someString)
Await.result(persistenceService.persist(testRecord), Duration.Inf)
Await.result(persistenceService.delete(testRecord.id), Duration.Inf)
Thread.sleep(1000) // Why do I need that to make the test succeeds?
val thrownException = intercept[RecordNotFoundException] {
Await.result(persistenceService.read(testRecord.id), Duration.Inf)
}
thrownException.getMessage should include(testRecord._id.toString)
}
}
}
And the read and delete methods with the code initializing connection to db (part of the constructor):
class MongoPersistenceService[R](url: String, port: String, databaseName: String, collectionName: String) {
val driver = MongoDriver()
val parsedUri: Try[MongoConnection.ParsedURI] = MongoConnection.parseURI("%s:%s".format(url, port))
val connection: Try[MongoConnection] = parsedUri.map(driver.connection)
val mongoConnection = Future.fromTry(connection)
def db: Future[DefaultDB] = mongoConnection.flatMap(_.database(databaseName))
def collection: Future[BSONCollection] = db.map(_.collection(collectionName))
def read(id: BSONObjectID): Future[R] = {
val query = BSONDocument("_id" -> id)
val readResult: Future[R] = for {
coll <- collection
record <- coll.find(query).requireOne[R]
} yield record
readResult.recover {
case NoSuchResultException => throw RecordNotFoundException(id)
}
}
def delete(id: BSONObjectID): Future[Unit] = {
val query = BSONDocument("_id" -> id)
// first read then call remove. Read will throw if not present
read(id).flatMap { (_) => collection.map(coll => coll.remove(query)) }
}
}
So to make my test pass, I had to had a Thread.sleep right after waiting for the delete to complete. Knowing this is evil usually punished by many whiplash, I want learn and find the proper fix here.
While trying other stuff, I found instead of waiting, entirely closing the connection to the db was also doing the trick...
What am I misunderstanding here? Should a connection to the db be opened and close for each call to it? And not do many actions like adding, removing, updating records with one connection?
Note that everything works fine when I remove the read call in my delete function. Also by closing the connection, I mean call close on the MongoDriver from my test and also stop and start again embed Mongo which I'm using in background.
Thanks for helping guys.
Warning: this is a blind guess, I've no experience with MongoDB on Scala.
You may have forgotten to flatMap
Take a look at this bit:
collection.map(coll => coll.remove(query))
Since collection is Future[BSONCollection] per your code and remove returns Future[WriteResult] per doc, so actual type of this expression is Future[Future[WriteResult]].
Now, you have annotated your function as returning Future[Unit]. Scala often makes Unit as a return value by throwing away possibly meaningful values, which it does in your case:
read(id).flatMap { (_) =>
collection.map(coll => {
coll.remove(query) // we didn't wait for removal
() // before returning unit
})
}
So your code should probably be
read(id).flatMap(_ => collection.flatMap(_.remove(query).map(_ => ())))
Or a for-comprehension:
for {
_ <- read(id)
coll <- collection
_ <- coll.remove(query)
} yield ()
You can make Scala warn you about discarded values by adding a compiler flag (assuming SBT):
scalacOptions += "-Ywarn-value-discard"
I have the following code:
Some(db.run(unionPermissionQuery.result).map(_.map(_.name).toList))
and I get the following error:
[error] found : scala.concurrent.Future[List[String]]
[error] required: List[String]
[error] Some(db.run(unionPermissionQuery.result).map(_.map(_.name).toList)),
[error] ^
[error] one error found
So I suppose I have to convert the Future[List[String]] to List[String]. I am new to scala sorry if that's too easy.
Full code:
def find(loginInfo: LoginInfo): Future[Option[models.admin.User]] = {
val userQuery = for {
dbLoginInfo <- loginInfoQuery(loginInfo)
dbUserLoginInfo <- Userlogininfo.filter(_.logininfoid === dbLoginInfo.id)
dbUser <- User.filter(_.userid === dbUserLoginInfo.userid)
} yield dbUser
db.run(userQuery.result.headOption).map { dbUserOption =>
dbUserOption.map { user =>
val permissionQuery = for {
dbUserPermission <- Userpermission.filter(_.userid === user.userid)
dbPermission <- Permission.filter(_.id === dbUserPermission.permissionid)
} yield dbPermission
val rolePermissionQuery = for {
dbUserRole <- Userrole.filter(_.userid === user.userid)
dbRole <- Role.filter(_.id === dbUserRole.roleid)
dbRolePermission <- Rolepermission.filter(_.roleid === dbRole.id)
dbPermission <- Permission.filter(_.id === dbRolePermission.permissionid)
} yield dbPermission
val unionPermissionQuery = permissionQuery union rolePermissionQuery
models.admin.User(
UUID.fromString(user.userid),
user.firstname,
user.lastname,
user.jobtitle,
loginInfo,
user.email,
user.emailconfirmed,
Some(db.run(unionPermissionQuery.result).map(_.map(_.name).toList)),
user.enabled)
}
}
I just want to get the user and then fill all permissions. Individual permissions and permissions inherited by the role assigned to the user.
Is it better to get the user and then perform another request to get the permissions based on the user id? I don't think so.
General information on futures
You will find all information on Futures you need on
http://docs.scala-lang.org/overviews/core/futures.html.
A possible approach is awaiting the result, but it is not good to use in a prod application.
val myFutureResult : Future[T] = Future {...}
val myResult : T = Await.result(myFutureResult, secondsToWait seconds)
Normally instead of awaiting the result and storing it in a variable you can map the future and compose it and only use Await at the last moment.
val myFutureResult : Future[T] = Future {...}
def myFunctionOnT(in: T) = ...
for {res <- myFutureResult } yield myFunctionOnT(res)
Play and Futures
Play itself can handle Future[T]by default by using Action.async {} instead of Action{} here you can find more: https://www.playframework.com/documentation/2.5.x/ScalaAsync
This information applied to extended question
db.run(unionPermissionQuery.result).map(_.map(_.name).toList).map { permission =>
models.admin.User(
UUID.fromString(user.userid),
user.firstname,
user.lastname,
user.jobtitle,
loginInfo,
user.email,
user.emailconfirmed,
Some(permission),
user.enabled)
}
I'm answering the question in the title.
The only way to implement the function Future[A] => A in a sensible way (and without access to a time-machine) is to await the completion of the future, as a value Future[A] indicates a value of type A will yield in the future.
Check your library for the correct method to wait. For the Scala std lib it is .result. As commented by Jean, the correct way for the Scala std lib is to use Await.result().
Note that waiting is a blocking method call. If you want to avoid this (and there are some good reasons to do so), you should check out Andreas' answer.
According to the Scala Language Specification (§6.19), "An enumerator sequence always starts with a generator". Why?
I sometimes find this restriction to be a hindrance when using for-comprehensions with monads, because it means you can't do things like this:
def getFooValue(): Future[Int] = {
for {
manager = Manager.getManager() // could throw an exception
foo <- manager.makeFoo() // method call returns a Future
value = foo.getValue()
} yield value
}
Indeed, scalac rejects this with the error message '<-' expected but '=' found.
If this was valid syntax in Scala, one advantage would be that any exception thrown by Manager.getManager() would be caught by the Future monad used within the for-comprehension, and would cause it to yield a failed Future, which is what I want. The workaround of moving the call to Manager.getManager() outside the for-comprehension doesn't have this advantage:
def getFooValue(): Future[Int] = {
val manager = Manager.getManager()
for {
foo <- manager.makeFoo()
value = foo.getValue()
} yield value
}
In this case, an exception thrown by foo.getValue() will yield a failed Future (which is what I want), but an exception thrown by Manager.getManager() will be thrown back to the caller of getFooValue() (which is not what I want). Other possible ways of handling the exception are more verbose.
I find this restriction especially puzzling because in Haskell's otherwise similar do notation, there is no requirement that a do block should begin with a statement containing <-. Can anyone explain this difference between Scala and Haskell?
Here's a complete working example showing how exceptions are caught by the Future monad in for-comprehensions:
import scala.concurrent._
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Try, Success, Failure}
class Foo(val value: Int) {
def getValue(crash: Boolean): Int = {
if (crash) {
throw new Exception("failed to get value")
} else {
value
}
}
}
class Manager {
def makeFoo(crash: Boolean): Future[Foo] = {
if (crash) {
throw new Exception("failed to make Foo")
} else {
Future(new Foo(10))
}
}
}
object Manager {
def getManager(crash: Boolean): Manager = {
if (crash) {
throw new Exception("failed to get manager")
} else {
new Manager()
}
}
}
object Main extends App {
def getFooValue(crashGetManager: Boolean,
crashMakeFoo: Boolean,
crashGetValue: Boolean): Future[Int] = {
for {
manager <- Future(Manager.getManager(crashGetManager))
foo <- manager.makeFoo(crashMakeFoo)
value = foo.getValue(crashGetValue)
} yield value
}
def waitForValue(future: Future[Int]): Unit = {
val result = Try(Await.result(future, Duration("10 seconds")))
result match {
case Success(value) => println(s"Got value: $value")
case Failure(e) => println(s"Got error: $e")
}
}
val future1 = getFooValue(false, false, false)
waitForValue(future1)
val future2 = getFooValue(true, false, false)
waitForValue(future2)
val future3 = getFooValue(false, true, false)
waitForValue(future3)
val future4 = getFooValue(false, false, true)
waitForValue(future4)
}
Here's the output:
Got value: 10
Got error: java.lang.Exception: failed to get manager
Got error: java.lang.Exception: failed to make Foo
Got error: java.lang.Exception: failed to get value
This is a trivial example, but I'm working on a project in which we have a lot of non-trivial code that depends on this behaviour. As far as I understand, this is one of the main advantages of using Future (or Try) as a monad. What I find strange is that I have to write
manager <- Future(Manager.getManager(crashGetManager))
instead of
manager = Manager.getManager(crashGetManager)
(Edited to reflect #RexKerr's point that the monad is doing the work of catching the exceptions.)
for comprehensions do not catch exceptions. Try does, and it has the appropriate methods to participate in for-comprehensions, so you can
for {
manager <- Try { Manager.getManager() }
...
}
But then it's expecting Try all the way down unless you manually or implicitly have a way to switch container types (e.g. something that converts Try to a List).
So I'm not sure your premises are right. Any assignment you made in a for-comprehension can just be made early.
(Also, there is no point doing an assignment inside a for comprehension just to yield that exact value. Just do the computation in the yield block.)
(Also, just to illustrate that multiple types can play a role in for comprehensions so there's not a super-obvious correct answer for how to wrap an early assignment in terms of later types:
// List and Option, via implicit conversion
for {i <- List(1,2,3); j <- Option(i).filter(_ <2)} yield j
// Custom compatible types with map/flatMap
// Use :paste in the REPL to define A and B together
class A[X] { def flatMap[Y](f: X => B[Y]): A[Y] = new A[Y] }
class B[X](x: X) { def map[Y](f: X => Y): B[Y] = new B(f(x)) }
for{ i <- (new A[Int]); j <- (new B(i)) } yield j.toString
Even if you take the first type you still have the problem of whether there is a unique "bind" (way to wrap) and whether to doubly-wrap things that are already the correct type. There could be rules for all these things, but for-comprehensions are already hard enough to learn, no?)
Haskell translates the equivalent of for { manager = Manager.getManager(); ... } to the equivalent of lazy val manager = Manager.getManager(); for { ... }. This seems to work:
scala> lazy val x: Int = throw new Exception("")
x: Int = <lazy>
scala> for { y <- Future(x + 1) } yield y
res8: scala.concurrent.Future[Int] = scala.concurrent.impl.Promise$DefaultPromise#fedb05d
scala> Try(Await.result(res1, Duration("10 seconds")))
res9: scala.util.Try[Int] = Failure(java.lang.Exception: )
I think the reason this can't be done is because for-loops are syntactic sugar for flatMap and map methods (except if you are using a condition in the for-loop, in that case it's desugared with the method withFilter). When you are storing in a immutable variable, you can't use these methods. That's the reason you would be ok using Try as pointed out by Rex Kerr. In that case, you should be able to use map and flatMap methods.