Gearing towards Ox
Tomasz Godzik @ VirtusLab
- Structured concurrency and how it relates to "direct" Scala?
- Intro to Ox
- Intro to Gears
- Other approaches
- Features comparison
- Notable differences
- Questions?
Agenda
Structured Concurrency and Direct Style
Threads
def printMessages(threadName: String): Unit =
for i <- 1 to 5 do
println(s"$threadName: Message $i")
Thread.sleep(1000)
@main def threads =
val thread1 = new Thread(() => printMessages("Thread 1"))
val thread2 = new Thread(() => printMessages("Thread 2"))
thread1.start()
thread2.start()
Threads
- no correct way to cancel the thread
- passing values is not easy
- the Threads can leak, they might running in the background even when you have no need for them anymore
Future
trait Api:
def get(id: Int): Option[String]
class Service(getApi: Future[Option[Api]]):
def calculate: Future[Option[String]] =
getApi.map{
apiOpt =>
apiOpt.flatMap{
api => api.get(1)
}
}
Future
trait Api:
def get(id: Int): Future[Option[String] ]
class Service(getApi: Future[Option[Api]]):
def calculate: Future[Option[String]] =
getApi.flatMap{
apiOpt =>
apiOpt.map{
api => api.get(1)
}.getOrElse(Future.successful(None))
}
Future
It leaks everywhere and you're not really interested in it for your logic
We still can have future spawned outside our control.
There are different monad abstractions to deal with that, but they are far from trivial
Is there a better way?
Is there a better way?
Obviously!
Structured Concurrency
Lifetime of a thread is determined by the syntactic structure of the code.
We want to have a verifiable way that nothing escapes the current scope
We want cancelling, make sure everything is handled properly.
This essentially creates a tree structure.
Structured Concurrency
Structured Concurrency
It is already widely used in Cats/ZIO
Functional frameworks require monads and ways of composing them.
Higher and more complicated abstractions.
Structured Concurrency
This is where direct style comes in!
Direct style (not Scala)
var running = true;
while (running) {
var time = await window.animationFrame;
context.clearRect(0, 0, 500, 500);
context.fillRect(time % 450, 20, 50, 50);
}
- We are basically back to imperative programming.
- Code is more concise and readable
- Better debugability since we can use all the normal tools
- Monads do not pollute the code
Direct style
Is direct style the solution for everything?
No
but
it's simpler
Ox
Ox
- Developed by SoftwareMill
- Based on project Loom
- Only Scala 3
-
concurrency - developer-friendly structured concurrency
-
error management: retries, timeouts, a safe approach to error propagation, safe resource management
-
scheduling & timers
-
resiliency: circuit breakers, bulkheads, rate limiters, backpressure
Focus
Basic example
//> using dep "com.softwaremill.ox::core:0.2.2"
import ox.*
@main def main =
supervised:
val hello = forkUser:
print("Hello")
val world = forkUser:
hello.join()
println(", world!")
world.join()
Gears
- cross-platform high-level asynchronous code
- direct-style Scala and structured concurrency
- featureful primitives and expose a simple direct-style API.
Focus
Basic example
//> using dep "ch.epfl.lamp::gears::0.2.0"
import gears.async.*
import gears.async.default.given
@main def main() =
Async.blocking:
val hello = Future:
print("Hello")
val world = Future:
hello.await
println(", world!")
world.await
Other approaches
In Scala - ZIO /Cats
Most existing Scala frameworks do represent structured concurrency, but not direct style.
They might be considered more difficult for beginners.
In Scala - ZIO direct
object EasyRacerClient extends ZIOAppDefault:
def scenario1(scenarioUrl: Int => String) =
defer:
val url = scenarioUrl(1)
val req = Client.request(Request.get(url))
val winner = req.race(req).run
winner.body.asString.run
In Scala - Cats async-await
import cats.effect.IO
import cats.effect.cps._
import scala.concurrent.duration._
val io = IO.sleep(50.millis).as(1)
val program: IO[Int] = async[IO] { io.await + io.await }
Kotlin - Coroutines
fun main() = runBlocking {
launch {
delay(1000L)
println("World!")
}
println("Hello")
}
Javascript Async
function resolveAfter2() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('resolved');
}, 2000);
});
}
async function asyncCall() {
console.log('calling');
const result = await resolveAfter2();
console.log(result);
}
Rust Tokio
use mini_redis::{client, Result};
#[tokio::main]
async fn main() -> Result<()> {
// Open a connection to the mini-redis address.
let mut client = client::connect("127.0.0.1:6379").await?;
// Set the key "hello" with value "world"
client.set("hello", "world".into()).await?;
// Get key "hello"
let result = client.get("hello").await?;
println!("got value from the server; result={:?}", result);
Ok(())
}
More examples
OX and Gears
The features
Both Ox and Gear are direct only and focus on making that experience work for users.
Unlike monadic approach, there are no longer descriptions of the program, but rather the program itself.
Problem
I want to run parallel computations to make my code faster
Ox
import ox.*
import scala.concurrent.duration.*
def computation(): Int =
sleep(2.seconds)
2
@main def multi =
val result: (Int, Int) = par(computation(), computation())
def computations = Seq.fill(20)(() => computation())
val resultMap: Seq[Unit] =
computations.mapPar(5)(str => println(str()))
val resultForeach: Unit =
computations.foreachPar(5)(str => println(str()))
Gears
import gears.async.*
import gears.async.default.given
import scala.concurrent.duration._
def computation(using Async.Spawn) = Future:
AsyncOperations.sleep(2.second)
println("Hello")
@main def multi() =
Async.blocking:
val computations: Seq[() => Future[Unit]] =
Seq.fill(20)(() => computation)
val resMap: Seq[Unit] =
computations.map(fut => fut()).awaitAll
val resGrouped: Unit =
computations.grouped(5).foreach:
futs => futs.map(fut => fut()).awaitAll
Problem
I want to get a service or from the database whichever is faster
Ox
import ox.*
import scala.concurrent.duration._
@main def racing =
def service: String =
sleep(2.seconds)
"Hello service"
def database: String =
sleep(2.seconds)
"Hello database"
println(race(service, database))
Gears
import gears.async.*
import gears.async.default.given
import scala.concurrent.duration._
@main def racing() =
Async.blocking:
def service: Future[String] =
Future:
AsyncOperations.sleep(2001)
"Hello service"
def database: Future[String] =
Future:
AsyncOperations.sleep(2001)
"Hello database"
println(Async.race(service, database).await)
Problem
I want to cancel or timeout long running request
Ox
import ox.*
import scala.concurrent.duration._
import scala.util.Try
@main def timeoutCancel =
def computation: Int =
sleep(2.seconds)
println("stopped")
1
println(Try(timeout(1.second)(computation)))
println(Try(timeout(3.seconds)(computation)))
supervised:
val cancellable = forkCancellable:
while true do
println("tick")
sleep(1.second)
forkUser:
sleep(4.seconds)
cancellable.cancel()
Gears
import gears.async.*
import gears.async.default.given
import scala.concurrent.duration._
import scala.util.Try
@main def timeoutCancel() =
Async.blocking:
def computation: Int =
AsyncOperations.sleep(2.seconds)
println("stopped")
1
println(Try(withTimeout(1.seconds)(computation)))
println(Try(withTimeout(3.seconds)(computation)))
val future = Future:
while true do
println("tock")
AsyncOperations.sleep(1.second)
Future:
AsyncOperations.sleep(4000)
future.cancel()
future.await
Problem
I want to properly handle any exceptions
Ox
@main def exceptionsOx() =
supervised:
def computation(withException: Option[String]): Int =
sleep(2.seconds)
withException match
case None => 1
case Some(value) =>
throw new Exception(value)
val fork1 = fork:
computation(withException = None)
val fork2 = fork:
computation(withException = Some("Oh no!"))
val fork3 = fork:
computation(withException = Some("Oh well.."))
println(fork1.join())
Ox
@main def exceptionsOxBonus() =
val res: Either[Throwable, Int] =
supervisedError(ox.EitherMode[Throwable]()):
def computation(withException: Option[String]) =
sleep(2.seconds)
withException match
case None => Right(1)
case Some(value) =>
Left(Exception(value))
val fork1 = forkError:
computation(withException = None)
val fork2 = forkError:
computation(withException = Some("Oh no!"))
val fork3 = forkError:
computation(withException = Some("Oh well.."))
println(fork1.join())
println(fork2.join())
println(fork3.join())
Right(1)
println("Hello!")
println(res)
Gears
@main def exceptionsGears() =
Async.blocking:
def computation(withException: Option[String]): Int =
AsyncOperations.sleep(2.seconds)
withException match
case None => 1
case Some(value) =>
throw new Exception(value)
val future1 = Future:
computation(withException = None)
val future2 = Future:
computation(withException = Some("Oh no!"))
val future3 = Future:
computation(withException = Some("Oh well.."))
future1.await
future2.awaitResult
future3.await
Problem
I want retry anything that fails
Ox
import ox.resilience._
def request(): Int =
sleep(1.second)
if Random.nextBoolean() then 100
else
println("Ups!")
throw new Exception("Bad!")
@main def retryOx() =
val policy: RetryPolicy[Throwable, Int] =
RetryPolicy.backoff(3, 100.millis, 5.minutes, Jitter.Equal)
println(retry(policy)(request()))
println(
retryEither(policy)(
Try(request()).toEither
)
)
println(
retryWithErrorMode(UnionMode[Throwable])(policy)(
Try(request()) match
case Failure(exception) => exception
case Success(value) => value
)
)
Gears
@main def retryGears() =
def request()(using Async): Int =
AsyncOperations.sleep(1000)
if Random.nextBoolean() then 100
else
println("Ups!")
throw new Exception("Bad!")
Async.blocking:
val result = Retry.untilSuccess
.withMaximumFailures(5)
.withDelay(
Delay.backoff(
maximum = 1.minute,
starting = 1.second,
jitter = Jitter.full
)
)(request())
println(result)
Problem
I want communicate some values between threads
Ox
import ox.channels.Channel
@main def channelsOx() =
val channel = Channel.buffered[String](5)
supervised:
val sender = forkUser:
sleep(1.second)
c2.send("Hello!")
sleep(3.second)
c2.send("World!")
val receiver = forkUser:
sleep(1.second)
println(c2.receive())
sleep(1.second)
println(c2.receive())
sender.join()
receiver.join()
Gears
import ox.channels.Channel
@main def channelsGears() =
val channel = BufferedChannel[String](5)
Async.blocking:
val sender = Future:
AsyncOperations.sleep(1.second)
channel.send("Hello!")
AsyncOperations.sleep(3.second)
channel.send("World!")
val receiver = Future:
AsyncOperations.sleep(1.second)
println(channel.read()) // Either[Closed, T]
AsyncOperations.sleep(1.second)
println(channel.read()) // Either[Closed, T]
sender.await
receiver.await
OX and Gears
Summary
Ox
Gears
Write direct style now
Fundations for capture calculus and experiments
Vs
Ox
Gears
JDK 21+
JDK 21+ and Native
Vs
Ox
Gears
Wider scope than just concurrency
Foundations for more advanced libraries
Vs
Ox
Gears
Operate on thunks => T and () => T
Custom Future
Vs
Ox
Gears
Anything fails -> all fail
Fail only if you await
Vs
Ox
Gears
Error Modes
Eithers mostly
Vs
Ox
Gears
One scope to rule them all (Ox)
Ability to spawn and to suspend separate (Async, Async.Spawn)
Vs
This presentation
Sample repository
Gearing towards Ox
By Tomek Godzik
Gearing towards Ox
- 92