Gearing towards Ox

Tomasz Godzik @ VirtusLab

  1. Structured concurrency and how it relates to "direct" Scala?
  2. Intro to Ox
  3. Intro to Gears
  4. Other approaches
  5. Features comparison
  6. Notable differences
  7. 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(())
}

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
  

github.com/jamesward/easyracer

 

Great comparison of different structured concurrency implementations

More examples

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

Twitter:         @TomekGodzik,

Mastodon:   fosstodon.org/@tgodzik

Email:            tgodzik@virtuslab.com

Gearing towards Ox

By Tomek Godzik

Gearing towards Ox

  • 25