I've done that before. This approach requires several conditions:
you should make sure that user would actually pass object
- you can do this with e.g. inline def runFoo[A <: Foo & Singleton]: Unit
you CANNOT prevent user from doing something like
class Example1 {
def localFunction: Unit = {
object LocalDefinition extends Foo
runFoo[LocalDefinition]
}
}
class Example2 {
object LocalDefinition extends Foo
def localFunction: Unit = {
runFoo[LocalDefinition]
}
}
and this cannot be implemented as object
s - even though they are singletons - contains a references to enclosing classes. So this would still require a check inside a macro, and a compilation error with the message explaining why
in general - if you want to access object A extends Foo
, there has to be Class[A$]
available on the classpath that is available to the macro, so even things like
object LocalDefinition extends Foo
runFoo[LocalDefinition]
is off limits, since you cannot obtain an instance of something which didn't emitted a bytecode yet (and it didn't since the file is still being compiled as evidenced by the ongoing macro expansion)
Once we accept these limitations, we might hack something together. I already prototyped something like that a few years ago and used the results in my OSS library to let users customize how string comparison should work.
You start by drafting the entrypoint to the macro:
object Macro:
inline def runStuff[A <: Foo & Singleton](inline a: A): Unit =
${ runStuffImpl[A] }
import scala.quoted.*
def runStuffImpl[A <: Foo & Singleton: Type](using Quotes): Expr[Unit] =
???
Then, we might implement a piece of code that translates Symbol
name into Class
name. I'll use a simplified version which doesn't handle nested objects:
def summonModule[M <: Singleton: Type](using Quotes): Option[M] =
val name: String = TypeRepr.of[M].typeSymbol.companionModule.fullName
val fixedName = name.replace(raw"$$.", raw"$$") + "$"
try
Option(Class.forName(fixedName).getField("MODULE$").get(null).asInstanceOf[M])
catch
case _: Throwable => None
with that we can actually use the implementation in macro (if it's available):
def runStuffImpl[A <: Foo & Singleton: Type](using Quotes): Expr[Unit] = {
import quotes.*
summonModule[A] match
case Some(foo) =>
foo.doStuff()
'{ () }
case None =>
reflect.report.throwError(s"${TypeRepr.of[A].show} cannot be used in macros")
}
Done.
That said this macro typeclass pattern, as I'd call it, is pretty fragile, error-prone and unintuitive to user, so I'd suggest not using it if possible, and be pretty clear with explanation what kind of objects can go there, both in documentation as well as in error message. Even then it would be pretty much cursed feature.
I'd also recommend against it if you cannot tell why it works from reading the code - it would be pretty hard to fix/edit/debug this if one cannot find their way around classpaths, classloaders, previewing how Scala code translates into bytecode, etc.
Foo extend Singleton
that forces all clients to useobject
- Not sure if that helps with the macro stuff tho. - BTW, what is the point of this other than just manually callingdoStuff
?Singleton
, thanks. It communicates my intent better (although I can't extend it directly, it has to be a self-type). Unfortunately, I don't see how it helps with the macro itself (since the macro can figure that out even without explicitly adding it to the trait).doStuff
, the actual methods I have on those objects encapsulate different tree transformations (instances ofTreeMap
) that I want to apply to the code, and I want to let the client code choose which transformations to apply at the macro invocation site.inline def runFoo[A <: Foo & Singleton](inline foo: A): Unit
? Mind that you still have to get the type, then theSymbol
, turn it intoClass
and finally obtain an instance using reflection, while checking that the class is even available at the compile time (it won't be if user define thatobject
in the same scope as macro expansion).