Contents

Safe direct-style Scala: Ox 0.1.0 released webp image

After a couple of months spent in an experimental phase and several iterations around the design of structured concurrency scopes and error handling, we are pleased to announce the 0.1.0 release of the Ox project. What is Ox? The project's motto says it best:

Safe direct-style concurrency and resiliency for Scala on the JVM

Ox requires Java 21 and Scala 3. It provides high-level concurrency operations, proposes an approach to error handling, implements structured concurrency and resiliency through retries, provides utilities for direct style programming, enables safe resource handling within scopes, and implements fast, Go-like channels with many built-in combinators. And we're just starting!

We've also spent some time improving Ox's documentation, with introductory sections on direct style, structured concurrency, error handling and generally improving the structure.

Let's take a whirlwind tour of the available features! Or just head over to the repository, star it and try Ox for yourself! :)

The starting point is adding the dependency:

// sbt dependency
"com.softwaremill.ox" %% "core" % "0.1.0"

// scala-cli dependency
//> using dep "com.softwaremill.ox::core:0.1.0"

With that out of the way, we can start running things in parallel:

def computation1: Int = { sleep(2.seconds); 1 }
def computation2: String = { sleep(1.second); "2" }
val result1: (Int, String) = par(computation1, computation2)
// (1, "2")

par will start two virtual threads, which will run the provided computations. If any of them fails, the other will be interrupted (using JVM's interruption mechanism); however, the par method will only return once both branches have finished.

Similarly, we can timeout a computation:

def computation: Int = { sleep(2.seconds); 1 }
val result2: Either[Throwable, Int] = 
  catching(timeout(1.second)(computation))

If the built-in high-level concurrency operations are insufficient, you can always roll your own. Here, structured concurrency comes into play: a supervised scope defines the syntactical boundary, within which new threads (in Ox, these are called forks) can be started. When the scope completes, it is guaranteed that all such threads have finished. Hence, a method starting a scope never "leaks" threads, and has no threading effects visible from the outside.

For example, here, we start two top-level forks, which block the scope's completion (the default forks behave as daemon threads). As the second one throws an exception, the scope first interrupts all other running forks. Then, the exception is re-thrown:

supervised {
  forkUser {
    sleep(1.second)
    println("Hello!")
  }
  forkUser {
    sleep(500.millis)
    throw new RuntimeException("boom!")
  }
}

But, concurrency is not everything when it comes to direct style. Resiliency is equally important. While this area still needs a lot of development work, we're starting with a retry mechanism with flexible retry policies:

def computationR: Int = ???
retry(RetryPolicy.backoff(3, 100.millis, 5.minutes, Jitter.Equal))(
    computationR)

It is now well established that sharing memory between concurrently running computations is error-prone. That's why Ox proposes a different approach, using channels following Go's motto:

Do not communicate by sharing memory; instead, share memory by communicating.

Combined with high-level operators, known from reactive stream implementations, channels provide much flexibility and a developer-friendly interface. For example:

supervised {
  Source.iterate(0)(_ + 1) // natural numbers
    .transform(_.filter(_ % 2 == 0).map(_ + 1).take(10))
    .foreach(n => println(n.toString))
}

Channels also implement the select method known from Go, which guarantees that exactly one clause from the list of given clauses will be selected and completed:

val c = Channel.rendezvous[Int]
val d = Channel.rendezvous[Int]
select(c.sendClause(10), d.receiveClause)

Finally, Ox provides an implementation of boundary-break specialized for Eithers, with programmer-friendly error messages. Using for-comprehensions is one possibility when representing application errors that way; the other is to "unwrap" Either values using the .ok() extension method within an either: boundary:

val v1: Either[Int, String] = ???
val v2: Either[Long, String] = ???

val result: Either[Int | Long, String] = either:
  v1.ok() ++ v2.ok()

What's described above is just a glimpse of Ox's full feature set—please refer to the documentation to discover all utilities, structured concurrency, streaming, resiliency, and high-level concurrency operations.

We'd be grateful for your feedback. We have a community forum, and if you have an idea for a feature or spot a problem, just report it on GitHub.

I hope you'll enjoy programming Scala even more with the help of Ox!

Read: Designing a (yet another) retry API

Blog Comments powered by Disqus.