I am working on a library for use in tests, where I want to be able to have variables that can be redefined in a particular scope. This is inspired by let
in rspec.
I have something working, by consistently shadowing a variable called scope
(of type Scope
).
The Scope
type has
apply[T](String)(Scope => T)
method for defining child scopes, and evaluating the given block immediately with that child scope as an argumentlet
method for refining the value of a Scoped
variable within this scopeget
method for getting the value of a Scoped
variable's value within this scopeA dumbed-down example which works:
object MyTest extends Scopes with FunSuite {
val env = Scoped[String]("production")
val input = Scoped[Int]()
scope("when the input is zero") { scope =>
scope.let(input, 0)
test("the value equals zero") {
expect(scope.get(input) == 0)
}
scope("and the app is running in staging") { scope =>
scope.let(env, "staging")
test("the value still equals zero") {
expect(scope.get(input) == 0)
}
}
}
}
That works because the Scopes
trait provides val scope = Scope.root
, and as long as you consistently shadow the scope
variable every time you create a child scope, tests can refer to scope
and it'll be based on the lexical scope where they're defined.
The API would be less verbose if I methods could accept an implicit Scope
, and that does work (in scala 3) if I consistently write each nested scope as:
scope("nested scope") { implicit scope =>
// ...
}
But that's more verbose at the scope definition, even though it can improve brevity when accessing or redefining scoped variables.
I was hoping I could cut down on the boilerplate via macros. Essentially I want to automatically add the above implicit activeScope =>
boilerplate on each call to scope
. Then the user's code would look like:
scope("nested scope") {
// I can call a method taking (implicit scope: Scope)
}
I got this to typecheck by having an implicit val rootScope: Scope = ???
in the Scopes
trait, and then having a scope
macro which automatically introduces an implicit at the beginning of the block passed to it. The code generation part of the macro looks like this:
def contextCode[T](scope: Expr[Scope], s: Expr[String], block: Expr[T])(using Quotes, Type[T]): Expr[T] = {
'{
${scope}(${s}) { implicit activeScope =>
(${block})
}
}
}
This compiles, but it doesn't use the introduced implicit. I suspect that since typechecking happens prior to macro expansion, so does implicit resolution (so my new implicits are ignored).
Is there any way I can use macros to affect which Scope
value is selected based on the lexical scope? I'm willing to resort to some cunning tricks as long as they're reliable.
In terms of constraints, the outer scope
definition calls are all evaluated at object creation time, so I'd be OK with using some mutable state to track "the active scope" at definition time. The problem is that the code within test blocks needs to access the same scope, and that code runs much later (potentially in parallel), so the "active" scope in test bodies must be based on its lexical scope, not runtime state.
Oh, and I don't have any control over the test framework itself. That is, I can't (for example) modify the test
function to do anything special before running the test body.
You don't need macros but context function and some utilities (with inline
):
final case class Scoped private (
private val nesting: List[String] = Nil,
private var ctx: Map[String, String] = Map.empty
) {
// magic happens here ````````````````\/
def apply(name: String)(block: Scoped ?=> Any): Unit =
block(using this.copy(name :: nesting))
def let(input: String, value: String): Unit =
ctx = ctx + (input -> value)
def get(input: String): String = ctx.getOrElse(input, "")
}
object Scoped {
given Scoped = Scoped()
}
inline def scope(using s: Scoped): Scoped = s
println(s"$scope")
scope("foo") { // no need for `implicit scope =>`
scope.let("foo", "foo1")
println(s" ${scope.get("foo")}")
println(s" $scope")
scope("bar") { // overshadowing of given Scoped OOTB
scope.let("boo", "boo1")
println(s" ${scope.get("boo")}")
println(s" $scope")
}
println(s" $scope")
}
println(s"$scope")
(scastie)
Every time you use nested context function (?=>
) the given
inside overshadows the given
outside, so you can safely and predictably create a copy of given value, use it in nesting, and it won't affect the scope outside - which sounds like what you are trying to do.
I didn't bother making my Scoped
example generic, but it should give you some understanding how to overshadow given
s with in nested context functions.