Reputation: 91
I want to flatMap a Try[Option[A]] using some function that uses the value inside the Option to create another Try, and I want the solution to be simple and idiomatic. I have illustrated the problem with an example. The goal is to create a Option[Group] with members and events wrapped in a single Try that can contain errors from any of the three functions.
def getGroup(id: Long): Try[Option[Group]]
def getMembersForGroup(groupId: Long): Try[Seq[Member]]
def getMeetingsForGroup(groupId: Long): Try[Seq[Meeting]]
I find it difficult to flatMap from the Try returned by getGroup
to the Try from the member- and meeting-functions because there's an Option "in the way". This is what i have come up with so far:
getGroup(id).flatMap(
groupOpt => groupOpt.map(
group => addStuff(group).map(group => Some(group))
).getOrElse(Success(None))
)
def addStuff(g: Group): Try[Group] =
for {
members <- getMembersForGroup(g.id)
meetings <- getMeetingsForGroup(g.id)
} yield g.copy(members = members, meetings = meetings)
What I don't like about my solution is that I have to wrap the group returned by addStuff
in an Option to perform the getOrElse. At this point the type is Option[Try[Option[Group]]] which I think makes the solution difficult to understand at first glance.
Is there a simpler solution to this problem?
Upvotes: 5
Views: 536
Reputation: 877
Try this. You get to keep your for comprehension syntax as well as Failure information from any of the three calls (whichever fails first).
def getFullGroup(id: Long): Try[Option[Group]] =
getGroup(id).flatMap[Option[Group]] { _.map[Try[Group]]{ group =>
for {
meetings <- getMeetingsForGroup(id)
members <- getMembersForGroup
} yield group.copy(meetings = meetings, members = members)
}.fold[Try[Option[Group]]](Success(None))(_.map(Some(_)))
}
Note the type acrobatics at the end:
fold[Try[Option[Group]]](Success(None))(_.map(Some(_)))
It's hard to get right without type annotations and an IDE. In this particular case, that's not too bad, but imagine meetings and members depended on another nested try option which in turn depended on the original. Or imagine if you wanted to a comprehension on individual Meetings and Groups rather than using the entire list.
You can try using an OptionT monad transformer from cats or scalaz to stack Try[Option[Group]]
into a non-nested OptionT[Try, Group]
. If you use a monad transformer, it can look like this:
def getFullGroup(id: Long): OptionT[Try, Group] =
OptionT(getGroup(id)).flatMapF { group =>
for {
meetings <- getMeetingsForGroup(id)
members <- getMembersForGroup(id)
} yield group.copy(meetings = meetings, members = members)
}
}
For this particular case, there's not really much gain. But do look into it if you have a lot of this kind of code.
By the way, the boilerplate at the end of the first example that flips the Try and Option is called a sequence
. When it follows a map, the whole thing is called traverse
. It's a pattern that comes up often and is abstracted away by functional programming libraries. Instead of using OptionT, you can do something like:
def getFullGroup(id: Long): Try[Option[Group]] =
getGroup(id).flatMap[Option[Group]] { _.traverse { group =>
for {
meetings <- getMeetingsForGroup(id)
members <- getMembersForGroup
} yield group.copy(meetings = meetings, members = members)
}
}
(Generally, if you're mapping f
then flipping monads, you want to traverse with f
.)
Upvotes: 0
Reputation: 40500
You could use .fold
instead of .map.getOrElse
... That makes it a little bit nicer:
getGroup(id)
.flatMap {
_.fold(Try(Option.empty[Group])){
addStuff(_).map(Option.apply)
}
}
or write the two cases explicitly - that may look a little clearer in this case, because you can avoid having to spell out the ugly looking type signature:
getGroup(id).flatMap {
case None => Success(None)
case Some(group) => addStuff(group).map(Option.apply)
}
Upvotes: 1
Reputation: 18424
Cats has an OptionT
type that might simplify this: documentation here and source here.
Your example would be:
def getGroupWithStuff(id: Long): OptionT[Try, Group] = {
for {
g <- OptionT(getGroup(id))
members <- OptionT.liftF(getMembersForGroup(g.id))
meetings <- OptionT.liftF(getMeetingsForGroup(g.id))
} yield g.copy(members = members, meetings = meetings)
}
Upvotes: 3
Reputation: 967
You probably could simplify your getGroup call to:
getGroup(id).map(
groupOpt => groupOpt.flatMap(
group => addStuff(group).toOption
)
)
, however that would be at cost of ignoring potential failure info from addStuff call. If it is not acceptable then it is unlikely you could simplify your code further.
Upvotes: 0