It's a Cycle of Life

JVM 'object' lifecycle

  • val i = 1
    // Stack: push -> pop

  • val o = new Object
    // Heap: init -> use -> gc (?)

  • val t: new Thread(...); t.start()
    // Heap: init -> start -> run -> join ...

  • val session = DbSession.connect(..)
    // Heap: connect -> use <-> reconnect -> close ...

  • val tx = session.beginTransaction(..)
    // Heap: begin -> snapshot <-> rollback|commit|error

try-with-resources

interface AutoCloseable {
    void close() throws Exception;
}
var file = File.createTempFile("rcl-", ".tmp");

try(
    var fw = new FileWriter(file);
    var bw = new BufferedWriter(fw);
) {
    bw.write("Hello World!");
}

Interlude: Logger & Metrics

trait Logger extends AutoCloseable:
  def name: String
  def printLine(s: String): Unit = ???
  def close(): Unit = ???
end Logger

trait Metrics:
  def apply[A](metric: String)(f: => A): A
  def get: SortedMap[String, Metrics.Metric]
  def clear(): Unit
end Metrics

scala.util.Using (2.13+)

object Using:
  trait Releasable[-A]:
    def release(a: A): Unit

  given [A <: AutoCloseable]: Releasable[A]
    with
      def release(a: A): Unit = a.close()

  def apply[A: Releasable, B](a: A)(
    f: A => B
  ): B = ???
 
object Using: 
  def apply[A: Releasable, B](a: A)(
    f: A => B
  ): B = 
object Using: 
  def apply[A: Releasable, B](a: A)(
    f: A => B
  ): B = 
    var toThrow: Throwable = null
    try f(a)
    catch case NonFatal(e) =>
      toThrow = e
      null.asInstanceOf[B]
    finally
      try summon[Releasable[A]].release(a)
      catch case NonFatal(e) =>
        if toThrow ne null then toThrow.addSuppressed(e)
        else toThrow = e
      if toThrow ne null then throw toThrow
    end try
  end apply
def sum(x: Int, y: Int): Int = 
  Using(Logger("log")): log =>
    log.printLine(s"will sum x = $x and y = $y")
    x + y
scala> sum(2, 2)
 -- Printer 'log' is acquired.
log: will sum x = 2 and y = 2
 -- Printer 'log' is released
val res6: Int = 4
def sumN(n: Int): Seq[Int] =
  Using(Logger("log")): log =>
    Using(Metrics()): meter =>
      log.printLine(
        s"will sum $n ranges [i..$n] where i in [0..$n]"
      )
      (0 to n).map: i =>
        meter("vector"):
          log.printLine(s"will sum range [$i, $n]")
          (i to n).foldLeft(0): (acc, i) =>
            meter("sum")(acc + i)
object Using:
  def apply[A1: Releasable, A2: Releasable, B](
    a1: A1, a2: => A2
  )(f: (A1, A2) => B): B = 
    apply(a1): a1 => 
      apply(a2): a2 => 
        f(a1, a2)
def sumN(n: Int): Seq[Int] = 
  Using(Logger("log"), Metrics()): (log, meter) =>
    log.printLine(
      s"will sum $n ranges [i..$n] where i in [0..$n]"
    )
    
    val result = (0 to n).map: i =>
      meter("vector"):
        log.printLine(s"will sum range [$i, $n]")
        (i to n).foldLeft(0): (acc, i) =>
          meter("sum")(acc + i)
          
    log.printLine(s"metrics:")
    meter.get.foreach: (name, metric) =>
      import metric.*
      log.printLine(
        s"  $name -> $mean ± $variance ($count samples)"
      )
    
    result
scala> sumN(3)
 -- Printer 'log' is acquired.
log: will sum 3 ranges [i..3] where i in [0..3]
log: will sum range [0, 3]
log: will sum range [1, 3]
log: will sum range [2, 3]
log: will sum range [3, 3]
log: metrics:
log:   sum -> 193.2 ± 91.90516851624832 (10 samples)
log:   vector -> 65496.75 ± 70337.1617652255 (4 samples)
 -- Printer 'log' is released
val res6: Seq[Int] = Vector(6, 6, 5, 3)
object Metrics:
  def apply(): Metrics = ???
  def logging(): Metrics = 
    Using(Logger("metrics")): log =>
      Logging(Metrics(), log)
      
  private[Metrics] final class Logging(
      underlying: Metrics,
      log: Logger,
  ) extends Metrics:
    def apply[A](metric: String)(f: => A): A = underlying(metric)(f)
    def get: SortedMap[String, Metric]       = underlying.get
    def clear(): Unit =
      log.printLine("collected metrics:")
      get.foreach: (name, metric) =>
        import metric.*
        log.printLine(
          f"  $name%s -> $mean%.2f ± $variance%.2f ($count%d samples)"
        )
      underlying.clear()
  end Logging
def sumN_logging_uar(n: Int): Seq[Int] = 
  Using(
    Logger("log"), 
    Metrics.logging()
  ): (log, meter) =>
    log.printLine(...)
    (0 to n).map: i =>
      meter("vector"):
        log.printLine(...)
        (i to n).foldLeft(0): (acc, i) =>
          meter("sum")(acc + i)
scala> sumN_logging_uar(3)
 -- Printer 'log' is acquired.
 -- Printer 'metrics' is acquired.
 -- Printer 'metrics' is released
log: will sum 3 ranges [i..3] where i in [0..3]
log: will sum range [0, 3]
log: will sum range [1, 3]
log: will sum range [2, 3]
log: will sum range [3, 3]
metrics: !!! WARN !!! Use of printer 'metrics' after release.
metrics: collected metrics:
metrics: !!! WARN !!! Use of printer 'metrics' after release.
metrics:   sum -> 209.70 ± 117.62 (10 samples)
metrics: !!! WARN !!! Use of printer 'metrics' after release.
metrics:   vector -> 131325.25 ± 181341.81 (4 samples)
 -- Printer 'log' is released
val res7: Seq[Int] = Vector(6, 6, 5, 3)
object Metrics:
  def logging(log: Logger): Metrics =
    Logging(Metrics(), log)
def sumN_logging_using(n: Int): Seq[Int] =
  Using(Logger("log")): log =>
    Using(Metrics.logging(log)): meter =>
      log.printLine(...)
      (0 to n).map: i =>
        meter("vector"):
          log.printLine(...)
          (i to n).foldLeft(0): (acc, i) =>
            meter("sum")(acc + i)
scala> sumN_logging_using(3)
 -- Printer 'log' is acquired.
log: will sum 3 ranges [i..3] where i in [0..3]
log: will sum range [0, 3]
log: will sum range [1, 3]
log: will sum range [2, 3]
log: will sum range [3, 3]
log: collected metrics:
log:   sum -> 395.30 ± 233.92 (10 samples)
log:   vector -> 82291.75 ± 42930.94 (4 samples)
 -- Printer 'log' is released
val res8: Seq[Int] = Vector(6, 6, 5, 3)

scala.util.Using.Manager

object Using:
  object Manager:
    def apply[A](f: Manager => A): A = 
      Using(new Manager())(f(_))

  final class Manager private () extends AutoCloseable:
    private var closed  = false
    private var handles = List.empty[() => Unit]

    def apply[A: Releasable](a: A): a.type =
      if !closed then 
        val r = () => summon[Releasable[A]].release(a)
        handles = r :: handles
      else throw new IllegalStateException(
        "Manager has already been closed"
      )
      a

    def close(): Unit =
      ...

  end Manager
  

scala.util.Using.Manager

object Using:
  final class Manager private () extends AutoCloseable:
    def close(): Unit =
      closed = true

      val toRelease          = handles
      var toThrow: Throwable = null

      handles = null
      toRelease.foreach: release =>
        try release()
        catch
          case NonFatal(e) =>
            if toThrow ne null then e.addSuppressed(toThrow)
            toThrow = e

      if toThrow ne null then throw toThrow
    end close

  end Manager
  
def sumN_manager(n: Int): Seq[Int] = 
  Using.Manager: use =>
    val log   = use(Logger("log"))
    val meter = use(Metrics.logging(log))
    
    ...
scala> sumN_manager(3)
 -- Printer 'log' is acquired.
log: will sum 3 ranges [i..3] where i in [0..3]
log: will sum range [0, 3]
log: will sum range [1, 3]
log: will sum range [2, 3]
log: will sum range [3, 3]
log: collected metrics:
log:   sum -> 4243.90 ± 12186.67 (10 samples)
log:   vector -> 538371.75 ± 870094.07 (4 samples)
 -- Printer 'log' is released
val res9: Seq[Int] = Vector(6, 6, 5, 3)

What about Scala 3?

// Functions
def sum_implicit(x: Int, y: Int)(implicit log: Logger) = ...
def repeat(i: Int)(f: Logger => A): Vector[A] =
  val log = new Logger("repeat")
  (0 until i).map(_ => f(log))

// repeat(3)(_ => sum_implicit(2, 2))
repeat(3)(_ => sum(2, 2))

repeat(3) { implicit log =>
  sum_implicit(2, 2)
}
// Context Functions
def sum_implicit(x: Int, y: Int)(implicit log: Logger) = ...
def repeat(i: Int)(f: Logger ?=> A): Vector[A] =
  val log = new Logger("repeat")
  (0 until i).map(_ => f(using log))

repeat(3)(sum_implicit(2, 2))

repeat(3) { log ?=>
  sum_implicit(2, 2)
}
package managed

final class Manager private[managed] ():
  private var closed  = false
  private var handles = List.empty[() => Unit]

  private[managed] def handle(f: () => Unit): Unit =
    ...

  private[managed] def close(): Unit =
    ...
def manage[A](f: Manager ?=> A): A =
  val manager = Manager()

  try f(using manager)
  catch ...
  finally ...
def defer(f: => Unit)(using M: Manager): Unit = 
  M.handle(() => f)

def use[A](a: A)(using M: Manager, R: Using.Releasable[A]): A =
  defer(R.release(a))
  a
def sumN(n: Int): Seq[Int] = manage:
  val log   = use(Logger("log"))
  val meter = use(Metrics.logging(log))

  log.printLine(...)
  defer(log.printLine("'sumN' completed"))

  (0 to n).map: i =>
    meter("vector"):
      log.printLine(...)
      (i to n).foldLeft(0): (acc, i) =>
        meter("sum")(acc + i)
scala> sumN(3)
 -- Printer 'log' is acquired.
log: will sum 3 ranges [i..3] where i in [0..3]
...
log: will sum range [3, 3]
log: 'sumN' completed
log: collected metrics:
log:   sum -> 339.30 ± 186.86 (10 samples)
log:   vector -> 71306.50 ± 14343.61 (4 samples)
 -- Printer 'log' is released
val res1: Seq[Int] = Vector(6, 6, 5, 3)

Interlude: Bracket

// [A, B] =>> (() => A)(A => B)(A => ())
def bracket[A, B](acquire: => A)(
  use: A => B
)(release: A => Unit): B =
  val a = acquire

  try use(a)
  catch ...
  finally
    try release(a)
    catch ...
def sumN(n: Int): Seq[Int] =
  bracket:
    val log   = Logger("log")
    val meter = Metrics.logging(log)
    (log, meter)
  .apply: (log, meter) =>
    log.printLine(...)
    (0 to n).map: i =>
      meter("vector"):
        log.printLine(...)
        (i to n).foldLeft(0): (acc, i) =>
          meter("sum")(acc + i)
  .apply: (log, meter) =>
    meter.clear()
    log.close()

RAII

Resource acquisition is initialization:

  • acquire <= construct
  • release => destruct
trait Resource[A]:
  def allocate: (A, () => Unit)

  def use[B](f: A => B): B =
    val (a, release) = allocate

    try f(a)
    catch ...
    finally
      try release()
      catch ...

object Resource:
  def apply[A](acquire: => A)(release: A => Unit): Resource[A] = 
    new Resource[A]:
      def allocate: (A, () => Unit) =
        val a = acquire
        (a, () => release(a))
object Logger:
  def resource(name: String): Resource[Logger] =
    Resource(Logger(name))(_.close())
trait Resource[A]:
  def map[B](f: A => B): Resource[B] = 
    Resource.Map(this, f)
  def flatMap[B](f: A => Resource[B]): Resource[B] = 
    Resource.Bind(this, f)

object Resource:
  final class Map[A, B](
    underlying: Resource[A],
    f: A => B
  ) extends Resource[B]:
    def allocate: (B, () => Unit) =
      val (a, release) = underlying.allocate
      (f(a), release)

  final class Bind[A, B](
    underlying: Resource[A], 
    f: A => Resource[B]
  ) extends Resource[B]:
    def allocate: (B, () => Unit) =
      ...
object Resource:
  final class Bind[A, B](underlying: Resource[A], f: A => Resource[B])
    extends Resource[B]:
    def allocate: (B, () => Unit) =
      val (a, releaseA) = underlying.allocate
      try
        val (b, releaseB) = f(a).allocate
        val releaseBoth = () =>
          var toThrow: Throwable = null
          try releaseB()
          catch case NonFatal(e) => toThrow = e
          finally
            try releaseA()
            catch
              case NonFatal(e) =>
                if toThrow ne null then e.addSuppressed(toThrow)
                toThrow = e
          end try
          if toThrow ne null then throw toThrow
        (b, releaseBoth)
      catch
        case NonFatal(e) =>
          try releaseA()
          catch
            case NonFatal(e2) =>
              e.addSuppressed(e2)
          throw e
object Metrics:
  def resource(): Resource[Metrics] =
    Resource(Metrics())(_.clear())
    
  def lazyLogging(): Resource[Metrics] =
    resource().flatMap: origin =>
      val log = Logger.resource("metrics")
      Resource(LazyLogging(origin, log))(_.clear())  

  final class LazyLogging(
      underlying: Metrics,
      log: Resource[Logger],
  ) extends Metrics:
    def apply[A](metric: String)(f: => A): A = underlying(metric)(f)
    def get: SortedMap[String, Metric]       = underlying.get

    def clear(): Unit = log.use : log =>
      log.printLine("collected metrics:")
      get.foreach: (name, metric) =>
        import metric.*
        log.printLine(
          f"  $name%s -> $mean%.2f ± $variance%.2f ($count%d samples)"
        )
def sum(x: Int, y: Int): Int = 
  Logger.resource("log").use: log =>
    log.printLine(s"will sum x = $x and y = $y")
    x + y
scala> sum(2, 2)
 -- Printer 'log' is acquired.
log: will sum x = 2 and y = 2
 -- Printer 'log' is released
val res6: Int = 4
def sumN(n: Int): Seq[Int] =
  val resources = for
    log     <- Logger.resource("log")
    metrics <- Metrics.lazyLogging()
  yield (log, metrics)

  resources.use: (log, meter) =>
    log.printLine(...)
    (0 to n).map: i =>
      meter("vector"):
        log.printLine(...)
        (i to n).foldLeft(0): (acc, i) =>
          meter("sum")(acc + i)
 -- Printer 'log' is acquired.
log: will sum 3 ranges [i..3] where i in [0..3]
log: will sum range [0, 3]
log: will sum range [1, 3]
log: will sum range [2, 3]
log: will sum range [3, 3]
 -- Printer 'metrics' is acquired.
metrics: collected metrics:
metrics:   sum -> 420.50 ± 199.81 (10 samples)
metrics:   vector -> 75685.25 ± 40932.68 (4 samples)
 -- Printer 'metrics' is released
 -- Printer 'log' is released
val res4: Seq[Int] = Vector(6, 6, 5, 3)

Again, what about Scala 3?

def acquire[A](r: Resource[A])(using M: Manager): A =
  val (a, release) = r.allocate
  defer(release())
  a
def sumN_acquire(n: Int): Seq[Int] = manage:
  val log   = acquire(Logger.resource("log"))
  val meter = acquire(Metrics.lazyLogging())

  log.printLine(...)
  (0 to n).map: i =>
    meter("vector"):
      log.printLine(...)
      (i to n).foldLeft(0): (acc, i) =>
        meter("sum")(acc + i)
end sumN_acquire

A bit of metaprogramming

// 2.* Tuples: 
// Tuple1[+T1](_1: T1) ... Tuple22[+T1, .., +T22](...)
//
// 3.* Tuples

sealed trait Tuple
case object EmptyTuple extends Tuple
sealed trait NonEmptyTuple extends Tuple:
  def head: Head[this.type]
  def tail: Tail[this.type]

sealed abstract class *:[+H, +T <: Tuple]
  extends NonEmptyTuple

A bit of metaprogramming

// Match Types
type Head[X <: NonEmptyTuple] = X match
  case h *: _ => h
  
type Tail[X <: NonEmptyTuple] = X match
  case _ *: t => t
type Leaf[X] = X match
  case String      => Char
  case Iterable[t] => Leaf[t]
  case _           => X

def leaf[X](x: X): Leaf[X] = x match
  case s: String      => s.head
  case i: Iterable[t] => leaf(i.head)
  case _              => x
type ResourceParams[X <: NonEmptyTuple] <: NonEmptyTuple = 
  X match
    case Resource[a] *: EmptyTuple => a *: EmptyTuple
    case Resource[a] *: tail       => a *: ResourceParams[tail]
def managing[X <: NonEmptyTuple, A](rs: X)(
    f: ResourceParams[X] => Manager ?=> A
)(using ev: Tuple.Union[X] <:< Resource[?]): A = manage:
  def loop(rest: NonEmptyTuple, acc: Tuple): NonEmptyTuple = 
    (rest: @unchecked) match
      case (r: Resource[a]) *: EmptyTuple => 
        acc :* acquire(r)
      case (r: Resource[a]) *: (tail: NonEmptyTuple) => 
        loop(tail, acc :* acquire(r))

  f(loop(rs, EmptyTuple).asInstanceOf[ResourceParams[X]])
def sumN_manage(n: Int): Seq[Int] = 
  managing(
    Logger.resource("log"),
    Metrics.lazyLogging()
  ): (log, meter) =>
    log.printLine(...)
    (0 to n).map: i =>
      meter("vector"):
        log.printLine(...)
        (i to n).foldLeft(0): (acc, i) =>
          meter("sum")(acc + i)
List<Integer> sumN_manage(n: Int) {
  try(
    var log   = Logger.resource("log"),
    var meter = Metrics.lazyLogging()
  ) { ... }
}

Problem: use-after-release

def sumN(n: Int): Seq[Int] = manage:
  val log = acquire(Logger.resource("log"))
  val (seqMetrics, sumMetrics) = Metrics
    .loggingResource()
    .use: metrics =>
      (metrics[Int]("vector"), metrics[Int]("sum"))

  log.printLine(...)
  (0 to n).map: i =>
    seqMetrics:
      log.printLine(...)
      (i to n).foldLeft(0): (acc, i) =>
        sumMetrics:
          acc + i
end sumN
scala> sumN(3)
 -- Printer 'log' is acquired.
 -- Printer 'metrics' is acquired.
metrics: collected metrics:
 -- Printer 'metrics' is released
log: will sum 3 ranges [i..3] where i in [0..3]
log: will sum range [0, 3]
log: will sum range [1, 3]
log: will sum range [2, 3]
log: will sum range [3, 3]
 -- Printer 'log' is released
val res2: Seq[Int] = Vector(6, 6, 5, 3)

Answer: Caprese!

CAPabilities for RESources and Effects

object Metrics:
  def logging(log: Logger): Metrics =
    ...
    
val log: Logger      = Logger("log")
val metrics: Metrics = Metrics.logging(log)
// using option "-experimental"

import scala.language.experimental.captureChecking

object Metrics:
  def logging(log: Logger^): Metrics^{log} =
    ...
    
// won't compile
def createMetrics(): Metrics = 
  val log: Logger^           = Logger("log")
  val metrics: Metrics^{log} = Metrics.logging(log)

  metrics

Answer: Caprese!

// pure, captures nothing
def log1[A, B](l: Logger)(f: A -> B): A -> B = ...

// impure, captures ^{l} (logger)
def log2[A, B](l: Logger^)(f: A ->{l} B): A ->{l} B = ...

// impure, captures ^{cap} (anything)
def log3[A, B](l: Logger^)(f: A => B): A => B = ...
val l1: Logger = Logger("test")

log1(l1)((i: Int) => i.toString)
val l2: Logger^ = Logger("test")

log2(l1)((i: Int) => i.toString)
val meter: Metrics^ = Metrics.logging(l2)

log3(l1)((i: Int) => meter(i.toString))

Answer: Caprese!

trait Resource[A]:
  def allocate: (A, () -> Unit)

  def use[B](f: A^ => B): B = 
    ...
  
object Resource:
  def apply[A](
    acquire: -> A
  )(
    release: A -> Unit
  ): Resource[A] =
    ...
def sum_error(x: Int, y: Int): Int =
  val print = logger("log").use: log =>
    (s: String) => log.printLine(s)
  
  print(s"will sum x = $x and y = $y")
  x + y
$> scala-cli run project.scala captured.scala
Compiling project (Scala 3.4.2, JVM (21))
[error] ./captured.scala:105:17
[error] local reference log leaks into
[error] outer capture set of type parameter B of method use
[error]     val print = logger("log").use: log =>
[error]                 ^^^^^^^^^^^^^^^^^
@capability
final class Manager private[managed] ():
  self =>
  private var handles: List[() ->{self} Unit]^{self} = Nil

  private[captured] def handle(f: () ->{self} Unit): Unit =
   ...

  private[captured] def close(): Unit =
   ...
   
def defer(using M: Manager)(f: ->{M} Unit): Unit = 
  M.handle(() => f)
  def sumN(n: Int): Seq[Int] = manage:
    val log = acquire(logger("log"))
    log.printLine(...)
    (0 to n).map: i =>
        log.printLine(...)
        (i to n).foldLeft(0): (acc, i) =>
          acc + i

  def sumN_delay(n: Int): Seq[Int] = manage:
    val print =
      val log = acquire(logger("log"))
      (s: String) => log.printLine(s)

    print(...)
    (0 to n).map: i =>
        print(...)
        (i to n).foldLeft(0): (acc, i) =>
          acc + i
def sumN_error(n: Int): Seq[Int] =
    val print = manage:
      val log = acquire(logger("log"))
      (s: String) => log.printLine(s)
  
    print(...)
    (0 to n).map: i =>
        print(...)
        (i to n).foldLeft(0): (acc, i) =>
          acc + i
$> scala-cli run project.scala captured.scala
Compiling project (Scala 3.4.2, JVM (21))
[error] ./captured.scala:132:17
[error] local reference contextual$3 leaks into
[error] outer capture set of type parameter A of method manage
[error]     val print = manage:
[error]                 ^^^^^^

Inspirations

  • scala.util.Using
  • twitter.util.Managed
  • cats.effect.Resource
  • zio.Scope

Any Questions?!

It's a cycle of life

By Alexey Shuksto

It's a cycle of life

Handling of object and resource lifecycles in Scala

  • 36