Unsafe Lazy Resource.scala

| 2 minutes | Comments

import scala.util.control.NonFatal

/** Builds a "closeable" resource that's initialized on-demand.
  *
  * Works like a `lazy val`, except that the logic for closing
  * the resource only happens in case the resource was initialized.
  *
  * NOTE: it's called "unsafe" because it is side-effecting.
  * See homework.
  */
final class UnsafeLazyResource[A](
  initRef: () => A,
  closeRef: A => Unit,
) extends AutoCloseable {

  /** Internal state that works like a FSM:
    *  - `null` is for pre-initialization
    *  - `Some(_)` is an active resource
    *  - `None` is the final state, a closed resource
    */
  @volatile private[this] var ref: Option[A] = null
  
  /** 
    * Returns the active resources. Initializes it if necessary.
    *
    * @return `Some(resource)` in case the resource is available,
    *         or `None` in case [[close]] was triggered.
    */
  def get(): Option[A] =
    ref match {
      case null =>
        // https://en.wikipedia.org/wiki/Double-checked_locking
        this.synchronized {          
          if (ref == null) {
            try {
              ref = Some(initRef())
              ref
            } catch {
              case NonFatal(e) =>
                ref = None
                throw e
            }
          } else {
            ref
          }
        }
      case other =>
        other
    }
  
  override def close(): Unit =
    if (ref ne None) {
      val res = this.synchronized {
        val old = ref
        ref = None
        old
      }
      res match {
        case null | None => ()
        case Some(a) => closeRef(a)
      }  
    }
}

Example:

import java.io._

def openFile(path: File): UnsafeLazyResource[InputStream] =
  new UnsafeLazyResource(
    () => new FileInputStream(path),
    in => in.close()
  )

val lazyInput = openFile(new File("/tmp/file"))
// .. later
try {
  val in = lazyInput.get().getOrElse(
    throw new IllegalStateException("File already closed")
  )
  //...
} finally {
  lazyInput.close()
}

Homework #

  1. Try using an AtomicReference instead of synchronizing a var — not as obvious as you’d think — initialization needs protection, you’ll need an indirection 😉
  2. Try designing a pure API with Cats Effect’s Resource (you might need Ref and Deferred for your internals too)
| Written by
Tags: Scala | Snippet