Skip to content
A factory macro for anonymous final static lazy variables. Also comes with implicit caching.
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
.idea
project
src
.gitignore
LICENSE
README.adoc
build.sbt

README.adoc

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.

Caution
Alpha-quality software.

Usage

Artifact
"net.zygfryd" %% "cement" % "0.2.1"
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.

Turning all implicit Cemented[T] into implicit T is one import away:

def bar(implicit ev: Evidence) = ???

def foo(implicit ev: Cemented[Evidence]) = {
  import Cemented.unwrap
  bar
}

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?

The Scala compiler had no invokedynamic support back then. For 2.11 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 member and a couple methods.

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.

You can’t perform that action at this time.