I was experimenting with porting from implicit to using / given. With 100+ occurrences in the source file, it was mostly a question of mass find and replace, with some manual work needed on a few places.
To my surprise, after I have compiled the project and submitted the build to CI, integration tests were reporting a failure because of temporary state object leakage.
After about an hour of bisecting to pinpoint the cause, it was this kind of code:
When converted to given Options = state.options, the lifetime of concerned objects is changed, as given is always lazy and therefore the state variable is captured by the lambda passed to the Future.
The obvious solution is:
val options = state.options
given Options = options
Not that bad, but compared to implicit val options: Options = state.Options it seems a bit wordy. Is there some other way?
No, that’s the recommended idiom whenever you care about how the definition of an implicit is evaluated. In most cases you would not care, but sometimes you do. It’s actually just one word longer than the old style implicit definition.
inline def withGiven[T, X](x: T)(inline f: T ?=> X): X = f(using x)
inline def withGiven[A, B, X](a: A, b: B)(inline f: (A, B) ?=> X): X = f(using a, b)
inline def withGiven[A, B, C, X](a: A, b: B, c: C)(inline f: (A, B, C) ?=> X): X = f(using a, b, c)
withGiven(state.options) {
// given available here
}
As a non native English speaker – the keyword given gives me the impression that it is “already there”. Whereas giving would feed the suggestion that it will be synthesised on the fly for me. Although it is clearly defined in the language specification, such subtleties may lead to confusion, especially if you do not use Scala on an every day basis. The meaning of a keyword in the real world of natural languages influences our understanding.
From my experience with server-side applications, I wanted to highlight a couple of potential considerations when broadly replacing val with lazy val:
Latency: Deferring the initialization of many values to runtime could, in some scenarios, lead to unexpected increases in tail latency, particularly under concurrent load. This is something we might want to check with load tests, profile and monitor closely if we proceed.
Possible cyclic dependencies: The Scala compiler doesn’t flag cyclic dependencies involving lazy vals, which could potentially result in runtime deadlocks.
We should be mindful of this as we make these kind of decisions for broadly used language constructions like given in Scala 3.