Remote Calls
convert
andconvertWithKey
allow other higher-kinded types to be interleaved into a journey.
Sometimes it is necessary to perform some sort of out-of-band interaction during a journey, for example it might be that you need the user to input a code and you need to call an API to look up a value based upon that code.
To illustrate this with an example let us take the journey to calculate the number of days a person has been alive from earlier -
import ltbs.uniform._
import java.time._, format._
def dateOfBirth = for {
dateOfBirth <- ask[LocalDate]("date-of-birth")
daysAlive = LocalDate.now.toEpochDay - dateOfBirth.toEpochDay
_ <- tell[Long]("days-alive", daysAlive)
} yield dateOfBirth.format(
DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL)
)
If we wanted to modify this such that the calculation for daysAlive
is
done via some remote process we can instead define a ‘server’ like so -
import scala.concurrent.Future
trait Server {
def calculate(dob: LocalDate): Future[Long]
}
We can now pass the server as a parameter into the function.
In order to adapt the Future
to whatever type we end up
interpreting to we can use the convert
method -
def dateOfBirthRemote(server: Server) = for {
dateOfBirth <- ask[LocalDate]("date-of-birth")
daysAlive <- convert(server.calculate(dateOfBirth))
_ <- tell[Long]("days-alive", daysAlive)
} yield dateOfBirth.format(
DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL)
)
In this case we now call our server in our program. We must
now provide a server instance when calling dateOfBirthRemote
Converters
Our interpreter will need to know how to handle the conversion from a
Future
(in the case of our example) to whatever higher-kinded type
the interpreter uses.
For example, suppose our interpreter targets the Option
higher-kinded type. We would need to
show how to convert a Future[LocalDate]
into a Option[LocalDate]
.
Many interpreters will ship with support for common datatypes (for
example the generic web interpreter provides support for converting
Future
into WebMonad
).
If you have to provide your own conversion method, there are three main ways to do it.
Using a natural transformation
The most generic way to show the interpreter how to handle the
conversion is to provide a natural transformation (for example
Future ~> Option
) in implicit scope.
In this instance the same logic will be applied for converting a
Future[LocalDate]
to a Option[LocalDate]
as would be used to convert a
Future[Customer]
to a Option[Customer]
import cats.~>
implicit val converterOne = new (Future ~> Option) {
def apply[A](in: Future[A]): Option[A] = ???
}
Once this instance is in scope you will be able to interpret any
journey that uses convert
from a Future
with an interpreter
targeting Option
.
Using a function
This method provides a more fine-grained control than using natural transformations.
For example if we wanted to use different logic for
converting a LocalDate
as for a Customer
we could instead define
an implicit function from one type to the other.
case class Customer(name: String, age: Int)
implicit def f: Function[Future[LocalDate],Option[LocalDate]] = ???
implicit def f2: Function[Future[Customer],Option[Customer]] = ???
Using a keyed and typed converter
This approach provides the most flexibility but is the least generic. In this instance we can control the logic for the conversion not only based upon types but also based upon the step ID.
In order to get a step ID we need to provide one in our journey using
the convertWithKey
function -
def dateOfBirthRemoteStepped(server: Server) = for {
dateOfBirth <- ask[LocalDate]("date-of-birth")
daysAlive <- convertWithKey("remote-call-dob")(
server.calculate(dateOfBirth)
)
_ <- tell[Long]("days-alive", daysAlive)
} yield dateOfBirth.format(
DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL)
)
We can now implement a Converter
, and use the step ID to discern
between “remote-call-dob” and any other similar call.
implicit val converter = new Converter[Future, Option, LocalDate] {
def apply(
key: String,
in: () => Future[LocalDate]
): Option[LocalDate] = key match {
case "remote-call-dob" => ???
case _ => ???
}
}
We often use this third approach when we need to encode and cache the result (for example with web interpreters).