Any logic you can use in Scala to branch can also be applied to Uniform journeys.
when,unlessand their monoid variants provide convenient shorthand for some common interactions.
Branching
If no steps in a journey are dependent upon the result of a previous
one then using a for comprehension may be overkill and you may prefer
using cats.Applicative instead -
import ltbs.uniform._
import cats.implicits._
case class Person(name: String, age: Int)
def askPerson(id: String) = (
ask[String](s"$id-name"),
ask[Int](s"$id-age")
).tupled
(askPerson("sender"), askPerson("receiver")).tupled
But lets suppose we want an optional third Person in our tuple,
and we want to use branching -
(
askPerson("sender"),
askPerson("receiver"),
ask[Boolean]("use-cc") flatMap {
case true => askPerson("cc") map {_.some}
case false => pure(none[Person])
}
).tupled
In this case the journey would ask the user the same 4 questions
initially as senderAndReceiverApplicative, however it would then ask the user
for a Boolean. If they answer no then the journey would end with
_3 being None. If the user picked yes however then they would be
asked again for a name and age and this time _3 would be defined
(Some).
This could be used for all sorts of branching - you are not confined to booleans, or to using pattern matching.
def bigSpender = for {
spendAny <- ask[Boolean]("spendAny")
spendAmount <- if (spendAny) {
ask[Int]("spendAmount")
} else {
pure(0)
}
optSpender <- if (spendAmount > 100000)
(
ask[String]("name"),
ask[Int]("age")
).mapN(Person).map{_.some}
else
pure(none[Person])
} yield optSpender
Simplified branching
The specific use-case of using a Boolean to control an
Option comes up a lot, so uniform offers a special syntax for it.
when
when is a construct that can take either a Boolean directly or a
interaction that returns a Boolean (such as ask[Boolean]).
Used directly with a boolean it emits an option in the same behaviour as
optSpender above - that is it returns a Some[A] where when the
predicate is true, and a None when the predicate is false.
when will short-circuit the journey and not execute the ask[A]
in the event that the predicate returns false.
for {
add <- ask[Boolean]("add-person")
person <- ask[Person]("person") when add
} yield person
When taking a journey that returns a boolean the approach is the same but essentially it does not need an intermediary variable -
ask[Person]("person") when ask[Boolean]("add-person")
unless
unless is just when but with the predicate inverted.
sealed trait Booze
case object Beer extends Booze
case class Martini(olive: Boolean) extends Booze
ask[Booze]("choose-drink") unless ask[Int]("age").map(_ < 18)
emptyWhen and emptyUnless
Similar to when and unless is emptyWhen and emptyUnless,
this however only works if the
datatype you are asking for is a Monoid, in which case it will give
empty instead of None. The return datatype is kept the same
as the underlying ask.
For example -
ask[Int]("hoursWorked") emptyWhen ask[Boolean]("retired")
We can apply this in the context of our earlier program in order to simplify the code -
def bigSpender2 = for {
spendAmount <- ask[Int]("spendAmount") emptyUnless
ask[Boolean]("spendAny")
optSpender <- (
ask[String]("name"),
ask[Int]("age")
).mapN(Person) when (spendAmount > 100000)
} yield optSpender