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 val
s.
Supported Scala versions: 2.11*
, 2.12
, 2.13
, 3.0*
. Support for Scala 3.0
is experimental.
Caution
|
Alpha-quality software. |
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)
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")
}
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
- Why is there an asterisk next to
2.11
and3.0
? -
The Scala
2.11
and3.0
compilers have no support forinvokedynamic
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.