Any logic you can use in Scala to branch can also be applied to Uniform journeys.
when
,unless
and 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