scala3 + zio + dotty-cps-async field report

scala3 + zio + dotty-cps-async

I’ve put multiple, small but “smart”, LOB applications into production based on scala and zio.

Recently, I also did that with a scala3+zio project. Because of the amount of twisty, async logic, scala3 and zio made it easy for the key blocking and tackling areas.

What I like about scala3:

  • Fewer braces. This seems to make a huge difference in code comprehension. Reading code I wrote just yesterday is easier than with the less-braces model. I don’t know why but it just works.
  • Macros, types, extensions, improved type inference: I do not use alot of arcane scala features in my code (libraries have at it!). I have noticed I write less types, macros from other packages seem more useful and I seem to write alot less boilerplate cade.

What I like about zio:

  • I still like all the things I liked about zio, large list.
  • Most importantly, it takes care of most of my needs for solving my problems without going outside the system and having to glue things together–huge productivity boost. The size of the ecosystem is in itself a key value proposition. (i.e. network effect).

While I have written about it before, you should check out dotty-cps-async because it simplifies your code even more.

Here’s the problem.

Effectful code can be awful to write. Even with the value proposition of ZIO, writing effectful code is painful. JVM Loom should make that easier because it automatically recognizes blocking calls and switches to another virtual thread. You don’t need to “tag” computations as being effectful. This may (not guaranteed) make code easier to write. We’ll see!

Below is a snippet of code from a very simple function. It does not require inline use of any return values (e.g. 1 + async(getIntAsync()) --real simple. Each line is an effect. In this case, each effect is run one after the other, but I could perform them in parallel. Many functions I write have alternating, large sequential logic flows followed by bursts of parallel logic flows followed by sequential logic flows.

def validateEnvelope(envelope: Envelope) =
	for
		_ <- log.info(s"Validating envelope.")
		_ <- both(envelope.body.filename, envelope.body.content,
				PayloadValidationError(s"Bad file spec.")
		_ <- findPersonById(envelope.body.id)
				.someOrFail(PayloadValidationError(s"Entity id ${envelope.body.id} is invalid."))
	yield ()

You still pay a cost of syntax overhead. Not bad though, I can live with that.

Using zio’s *> syntax makes this lighter on syntax but I find that I forget the *> and it causes me more headaches then not when there are more than a few effects to chain together. In this small example of course it is not so bad.

def validateEnvelope(envelope: Envelope) =
	log.info(s"Validating envelope.") *>
	both(envelope.body.filename, envelope.body.content,
		PayloadValidationError(s"Bad file spec.") *>
	findPersonById(envelope.body.id)
		.someOrFail(PayloadValidationError(s"Entity id ${envelope.body.id} is invalid."))

Since I sometimes have to use python and typescript that have the async syntax, async is easy to remember. And if you think about, the syntax overhead is about the same as using a for-comprehension. We could argue about the need to “tag” async operations using the for-comprehension+zio effects and async and what-not, but syntatically they carry about the same programming burden. Also, async+effect types is still better than async+no effect types that causes all interfaces to have 2 variants, sync and async–ugh!

Here’s the dotty-cps-async verison with async:

def validateEnvelope(envelope: Envelope) =
	asyncZIO[Dependencies, Exception] {
		await(log.info(s"Validating envelope."))
		await(both(envelope.body.filename, envelope.body.content,
			PayloadValidationError(s"Bad file spec."))
		await(findPersonById(envelope.body.id)
			.someOrFail(PayloadValidationError(s"Entity id ${envelope.body.id} is invalid.")))
	}

To me, using async is easier to remember when switching between programming languages. Please take my comment at face value. I mostly use for-comprehensions still.

However, I like it even easier.

dotty-cps-async does the “automatic” detection of parallel/async calls based on the ZIO effect type “tagging:”

def validateEnvelope(envelope: Envelope) =
	asyncZIO[Dependencies, Exception] {
		log.info(s"Validating envelope.")
		both(envelope.body.filename, envelope.body.content,
			PayloadValidationError(s"Bad file spec.")
		findPersonById(envelope.body.id)
			.someOrFail(PayloadValidationError(s"Entity id ${envelope.body.id} is invalid."))
	}

I of course still need to tag the block where I want good things to happen. In this short function that one line of asyncZIO adds alot of % line count, but I have larger effect chains where the overhead is trivial.

Overall, less code and fewer mistakes.

What I like about dotty-cps-async:

  • Everything.

Money in the bank.

Comments

Popular posts from this blog

quick note on scala.js, react hooks, monix, auth

zio environment and modules pattern: zio, scala.js, react, query management

user experience, scala.js, cats-effect, IO