dotty+scala.js+async: interesting options
With the soon-to-be-released dotty 3.0.0-M1 (2020 Q4) and scala.js support rapidly improving (thanks @sjrd and team!), we can soon use scala.js with dotty.
One area that has always been interesting to watch is the development of language constructs for managing effects such as Future for scala and async/await for javascript. Many languages have async/await support. scala 2.13.3 came out with -Xasync
support that allows async { await(...) }
like syntax. This “desugaring” can be used in combination with existing syntax such standard for-comprehensions or monadic style (chained function calls). Hence, there are 3 approaches.
Working with javascript types, especially Promises, has always been problematic. For example, typescript 4.x recently introduced refinements so that Promise typing was better. Even dedicated languages such as typescript have had typing issues :-)
Directly manipulating js.Promise
s is important for front-end work as other effect libraries can be quite heavyweight. If your APIs all use js.Promise
it makes little sense to always be converting Promises to something else.
With dotty, we can use some new language features to make life easier While you could do the following in scala 2.x, this note is focused on dotty.
For scala 2.x, I had already created jshelpers
as part of the https://github.com/aappddeevv/scalajs-reaction project. This library adds extensions to js.Promise
.
With dotty, this is much cleaner and easier:
// from my jshelpers lib, reduced and dottified
type RVAL[A] = A | js.Thenable[A]
type RESOLVE[A, B] = js.Function1[A, RVAL[B]]
type REJECTED[A] = js.Function1[scala.Any, RVAL[A]]
extension[A,B](self: js.Promise[A]):
def jsThen(f: A => B|js.Thenable[B]): js.Promise[B] =
val onf: RESOLVE[A,B] = f
self.`then`[B](onf, js.undefined).asInstanceOf[js.Promise[B]]
def map(f: A => B): js.Promise[B] = jsThen(f)
def flatMap(f: A => js.Promise[B]): js.Promise[B] = jsThen(f)
extension [A](self: js.Promise[A]):
/** Tap into the result. */
def tapValue(f: A => Any): js.Promise[A] =
val onf: RESOLVE[A,A] = (a: A) => { f(a); a }
self.`then`[A](onf, js.undefined).asInstanceOf[js.Promise[A]]
/** Filter on the value. Return failed Thenable with
* NoSuchElementException if p => false.
*/
def filter(p: A => Boolean): js.Promise[A] =
val onf = js.Any.fromFunction1 { (a: A) =>
val result = p(a)
if (result) js.Promise.resolve[A](a)
else js.Promise.reject(new NoSuchElementException()).asInstanceOf[js.Promise[A]]
}.asInstanceOf[RESOLVE[A, A]]
self.`then`[A](onf, js.undefined).asInstanceOf[js.Promise[A]]
/** for-comprehension support. */
def withFilter(p: A => Boolean) = filter(p)
With these types of extensions, you can do for-comprehensions.
val x = for {
r <- js.Promise.resolve[Int](10)
} yield r
val finalEffect = x.tapValue(v => println(s"v: $v"))
We can also roll in async/await type syntax. I tried using -Xasync
but have run into a few issues. If you can help me, a project https://github.com/aappddeevv/scalajs-promise-async could use your attention. A scala.js issue was opened at https://github.com/scala-js/scala-js/issues/4249.
You can also use an emerging project, https://rssh.github.io/dotty-cps-async/index.html, that also provides async/await syntax. This project looks to be much more extensive than -Xasync
in that it will support higher order functions. This effort is similar to the scala 2.x monadless project in spirit.
If you build off the current dotty-cps-async
main branch which has scala.js support and use the dotty nightly build, you can then do:
given JSPromiseMonad as CpsMonad[js.Promise]:
def pure[T](t:T):js.Promise[T] = js.Promise.resolve[T](t)
def map[A,B](fa:js.Promise[A])(f: A=>B):js.Promise[B] = fa.map(f)
def flatMap[A,B](fa:js.Promise[A])(f: A=>js.Promise[B]):js.Promise[B] = fa.flatMap(f)
Then in your code:
def promisfy(v: Int) = js.Promise.resolve[Int](v)
def myFun() = async[js.Promise]:
val x = await(js.Promise.resolve[Int](1))
val y = await(promisfy(10))
val z = x + y + 1
println(s"z=$z")
z
Now you can mix and match everything together:
val x = for {
r <- myFun()
.map(result => result + 10)
} yield r
x.tapValue(v => println(s"myFun() + 10: $v"))
js.Promise
s start eagerly just like scala Future
's. To delay evaluation, use () => effect
which I do not show above.
Choose your approach, you have 3 good choices.
A bit more work is needed as there are a few variants of CpsMonad
that we can employ but the basic idea is the same. One could argue that async/await constructs are not much different than for-comprehensions but after writing typescript for a machine learning application server (serving up a front-end for a ML-powered application), I like anything that is even a bit easier to read and this works for me when I’m using javascript Promises.
That’s it!
P.S.
- Yes I have some println in my effects mixed in :-) Useful for debugging when trying something quick.
- You can use https://www.streamlit.io/ to create a ML application interface but streamlit is a bit techy for business users).
This comment has been removed by the author.
ReplyDelete