zygfryd / scala-zygf-cement   0.3.0

Apache License 2.0 GitHub

A factory macro for anonymous final static lazy variables. Also comes with implicit caching.

Scala versions: 3.x 2.13 2.12 2.11

cement

This is a tiny macro library for Scala allowing minimum overhead caching of computation results, based on its position in source code. JDK 8’s invokedynamic instructions are generated under the hood to remember the result of the first invocation and return them on subsequent invocations.

In essence, cement is a factory for anonymous static lazy vals.

Supported Scala versions: 2.11*, 2.12, 2.13, 3.0*. Support for Scala 3.0 is experimental.

Caution
Alpha-quality software.

Usage

Artifact
"net.zygfryd" %% "cement" % "0.3.0"
Import
import zygf.cement.{cement, Cemented}

Cementing expressions

Performing a snippet of initialization code only once has never been this easy. Only the first invocation of this method will compile the pattern, while subsequent invocations return a cached value.

import java.util.regex._

def words(text: String): Array[String] = {
  val sep = cement { Pattern.compile("\\s+") }
  sep.split(text)
}
Warning
Exercise caution as changing input variables will not cause the cemented expression to re-evaluate:
(1 to 3).map { i => cement(i) }.toList == List(1, 1, 1)

Cementing implicits

If you’re expecting to receive implicits that have a non-trivial cost to automatically generate, this library has your back:

def keepCalmAnd(implicit escapePlan: Cemented[EscapePlan]) = {
  escapePlan.value.execute()
}
Note
A separate Cemented[T] instance is created for each place of invocation, where an implicit Cemented[T] isn’t already provided.

To explicitly create an instance of Cemented[T]:

val cemented1: Cemented[_] = Cemented(expr) // performs cementing magic on expr
val cemented2: Cemented[_] = Cemented.wrap(expr) // performs no magic, simply allocates an instance

A more complete example and original motivation for creating this library is statically caching automatically created logger objects:

import org.apache.logging.log4j._
import zygf.cement.Cemented

object Logging
{
  implicit def makeAutoLogger(implicit scope: sourcecode.FullName): Logger = {
    LogManager.getLogger(scope.value)
  }

  def autoLogger(implicit logger: Cemented[Logger]) = logger.value
}

Two, not six, getLogger calls would have been made by the following code:

import Logging._

(1 to 3).foreach { _ =>
  autoLogger.info("foo")
  autoLogger.info("bar")
}

Cost

Retrieval of a previously cemented expression’s value incurs no allocations, dictionary lookups, exception handling or locking, it consists of:

  • one constant method handle call, possibly inlined

  • one branch on an instanceof check

  • one checked cast

FAQ

Why is there an asterisk next to 2.11 and 3.0?

The Scala 2.11 and 3.0 compilers have no support for invokedynamic in macros. We use a different approach, equally performant, but with a higher bytecode footprint. A new class is generated for every cemented call site, containing a static field.

What happens when an exception is thrown?

Exceptions aren’t remembered. The cemented computation will continue to get re-evaluated until it successfully returns a value.