user experience, scala.js, cats-effect, IO
This is not a complicated snippet but I wanted to ensure I remembered it.
When creating a UI, say react for browsers, you will run into the “fetch” data problem. When you fetch data, you typically have a component that performs the fetch and acts as a cache for the result. The fetch component renders a child and passes along the data that was fetched (or the fetch state, etc.).
Even with the upcoming react suspense mechanism that will make this a smoother user experience, you may have a flash of a “loading” indicator that shows for a moment before the content is fetched from the server. If the flash is too fast, it is visually disruptive. A shimmer mechanism for the UI my also not be the answer.
Similarly, if the fetch request takes too long, you will want it to time-out and show an error message that the data could not be fetched or whatever is appropriate for feedback in your application.
Generally, I use cats-effect IO
for my main effect.
Scala.js cats/cats-effect implicits
Since we are using scala.js, we need some implicits that are normally provided by IOApp
on the JVM. It’s save to put these in the top package of your app and use them everywhere since they all map back to just 2 JS functions.
// misc cats-effect implicits, usually provided by IOApp
// at call site you may need to `import IO._`
implicit val timerIO = IO.timer
import scala.concurrent.ExecutionContext.Implicits.global
implicit val cs = IO.contextShift(global)
Effectful UX functions
One way we can handle handling the types of UX effects described above when using scala.js is to use cats and some simple combinators. Let’s say we are using IO:
def runAtLeast[A](w: FiniteDuration, value: IO[A]) = {
import IO._
(Timer[IO].sleep(w), value).parMapN((_, v) => v)
}
Many people use the javascript async or bluebird. Unfortunately, async alters the function syntax for your effects in a very obtrusive way. Bluebird is much less intrusive. In scala, we essentially have defined a combinator that we can apply to any effect without changing the type signature which is essentially IO[A] => IO[A]
.
As a reference point, the javascript Promise version is:
async function timeoutResolve(ms) {
return Promise((resolve, reject) => setTimeout(resolve, ms))
}
// value: () => Promise, to delay computation
async function runAtLeast(ms, value) {
return Promise.all([value(), timeoutResolve(ms)])
}
Back to scala…you might think that we could use Timer[IO].sleep(delta) *> value
for enforcing a minimum run time but this expression won’t work because *>
is really just flatMap and flatMaps sequence the IO operations one after the other versus running them in parallel. The import IO._
is needed to bring in NonEmtyParallel[IO,Par]
into scope. Par
is the parallel “version” of IO needed to satisfy NonEmptyParallel[IO, Par]
. It’s a bit awkward but it works here.
The parMapN
evaluates both effects in parallel an discards the first value, the sleep return value which is Unit, and returns the second value, the value A that we want. If you had a large number of effects you could use a .parSequence
to run them in parallel and return all of the results, just like javascript’s Promise.all
.
For timing out, there are many ways to handle this including handling it directly in the HTTP client but an easy way is:
def runNoMoreThan[A](delta: FiniteDuration, value: IO[A]) =
Concurrent.timeout[IO, A](value, delta)
If you come from a javascript background these make look strange. But they are really just analogues of using js Promise.
// javascript Promise runNoMoreThan
async function timeoutReject(ms) {
return Promise((resolve, reject) => setTimeout(reject, ms))
}
// value: () => Promise
async function runNoMoreThan(ms, value) {
return Promise.race([value(), timeoutReject(ms)])
.then(tuple => return tuple[0])
}
// use .then/.catch to use value or catch timeout
It’s critical to note that the concept of cancelability is not present in a js Promise directly so its entirely possible that the javascript version does not truly cancel if the timeout expires. That’s why js libraries such Bluebird are popular but the true semantics for js Promise’s are not always easy to discern or remember.
Generic F[_] versions of the effectful functions
We can make these generic on F using the usual cats and cats-effect typeclass approach and add a syntax extension. We could also make the context bounds be standard implicit parameters but we will keep them as context bounds except for NonEmptyParallel because NonEmptyParallel requires 2 effects. This feels a bit inconsistent but its fine for the code below.
def runAtLeast[F[_]: ConcurrentEffect: Timer, G[_], A](
wait: FiniteDuration,
value: F[A])(
implicit x: NonEmptyParallel[F,G]) =
(Timer[F].sleep(wait), value).parMapN((_, v) => v)
def runNoMoreThan[F[_]: Concurrent: Timer, A](
delta: FiniteDuration,
value: F[A]) =
Concurrent.timeout[F, A](value, delta)
implicit class RichIO[A](ioa: IO[A]) {
import IO._
def atLeast(wait: FiniteDuration) = runAtLeast(wait, ioa)
def noMore(delta: FiniteDuration) = runNoMoreThan(delta, ioa)
}
The general versions are a bit trickier to write which is why it is always better to write the specific ones first then generalize, unless you are already good at using cats/cats-effect.
It’s also clear that this is alot more complicated than the pure js Promise version–but hey, it will run with any F that satisfies the context bounds.
Now we can do myScalaJSUXFetchIO.atLeast(1 seconds).noMore(10 seconds)
and our async operation is good to go.
Running
Remember that cats is functional and the above is a “value” which still needs to be run.
myScalaJSUXFetchIO
.atLeast(1 seconds)
.noMore(10 seconds)
.unsafeRunAsync{
case Right(v) => self.send("Success")
case Left(t) => self.send(Message("Error!"))
}
If you wanted to tease out the timeout error you could do that:
myScalaJSUXFetchIO
.atLeast(1 seconds)
.noMore(10 seconds)
.unsafeRunAsync{
case Right(v) => self.send(Message("Success"))
case Left(t: scala.concurrent.TimeoutException) => self.send(Message("Network may be down. Try again."))
case Left(t) => self.send(Message("Error while saving."))
}
I use scalajs-reaction for my react, scalajs. UI and self.send
sends a message to the reducer to add a message that is then displayed in the UI. I also have several other effectful combinators so that errors are consistently handled for both the UI and for logging but those are not shown here.
I think that working with js Promise is easy to remember and code–but it is difficult to get correct because the .then and .catch clauses can return a value or a Promise and its easy to return “void.” However, the js Promise API is small, its js oriented, you can just as easily access it in scala.js. If that works for your code and you do not need cats-effect or other better typed monadic structures, you should keep it simple. I do on occasions.
But I found that once you layer in bluebird/async library Promises and lodash to get the capabilities I get with scala, cats and cats-effect, I might as well use the scala idiomatic versions. In other words, it isn’t just about one or two simple effects and I would rather avoid common mistakes that area easier to make in js. I also find scala.js error handling semantics to be clearer about what to expect so I can build up my application-specific error handling approach.
Generally, unless you are a library author, just stick to the IO effect directly and it can be as easy as using Promises in javascript but with better clarity.
That’s it!
Comments
Post a Comment