If quasi-quotes are strings Scala has already quasi-quotes…
But maybe (made up) examples are more helpful.
val classNames = List("Foo", "Bar")
val code: ClassName => Expr[ClassDef] = s"""
class $_:
def baz = println("doing work")
"""
classNames
.map: templateVariable =>
code(templateVariable.toClassName)
.foreach:
_.materialize
Would such code work?
How does the IDE work inside the “quasi-quote”?
Does the compiler complain if I use $_
at any other place than where a class name is expected and valid?
What does actually materialize
do? Where will I find the Scala files with the generated classes? How are they included in my current project?
When I’m inside the generated classes (wherever they are) will the IDE be able to navigate back to the “quasi-quote” that defined them, or find use sides?
The current reality is that this does not work like that.
To do the same as that hypothetical code I need to
- Create a “TemplateClass”, which is actually a real class in some sub-project that doesn’t get published and is there only for the compile time magic
- Compile that TemplateClass, and than reread the generated TASTy
- Walk the TASTy, programmatically change the class name so it matches the “template variable” and than “pretty print” that TASTy back to some files on disk in another project; do that for all “template variables” [I don’t even remember this was possible for class names, and I didn’t need to fall back to do string replace in the pretty printed code snippets; would need to look it up again how I did it, but not now]
- Compile that project and depend on it where I need to use the generated code
Don’t ask me how to do that if there were circular dependencies between generated code and its use side…
You do all of that of course without nice IDE support, that could for example navigate from that generated code back to the actual “TemplateClass”.
Also everything is just stringly typed. A ClassName
does not exist. It’s just a string, which I could use somewhere in some API call or constructor.
The “template variables” are of course much more involved in reality. You have at least some Map
s of List
s, and you need to walk the TASTy a few times until all is replaced and transformed so you can generate and write out one instance of the code.
The whole approach is imho a hack. It works, yes, but it’s far from optimal. It’s definitely not ergonomic. It’s very fragile. (Change the “TemplateClass” or it’s surroundings and you break likely the TASTy walking code-generation code).
It’s also not declarative. I can’t put some placeholder at the place of the definition of the name of my “TemplateClass”. I need to call that class somehow, and than fish for exactly that string in the TASTy. There is no API for placeholders (like in this example the class name, but could be also method names, parameter names, type names, package names, and maybe some other things I forgot) which could be filled with (type safe) template variables.
A declarative approach would be also much more robust. Changing the templates would not break everything, it would just keep working.
Because doing this with the above approach is so extremely involved people just use string templates for code instead. I’m also back to doing that. Because it needs less machinery, and it’s almost trivial to declaratively replace some placeholder in a string template as Scala has a built-in feature for that. But it does not have that for code, despite “powerful macro features”.
Of course everything is than just a string and you have no IDE support at all, but that’s also the status quo if you do it the involved way.
What I’ve described is of course half a compiler pipeline. Just in user space, hold together by some build scripts… I think it would be much simpler to just use an already existing compiler, which has all the machinery already available, in much better shape than whatever one could hack into existence. Also the compiler has already an API to feed back info into the IDE. Something the home made solution can’t provide with realistic effort.
Another aspect I was thinking of:
Code generation can be actually seen as part of staged compilation. Just that code-gen happens at “negative stages”. The above example would expand the code template at stage “-1”. (In theory one could think of generated code that generates code which would be than something happening at stage “-2”. But never seen a use-case for that. Still, if the machinery were there this wouldn’t be to difficult to have also I guess). The point is: Maybe this would fit nicely into the current theoretic framework? “Just” expand it to negative stages, get code-gen with superior compiler / IDE support for free. (OK, it needs to do all the things I’ve described above, and that’s not “free”. But I think the building blocks are already there. If you can do it in user space it should be even simpler to implement with the tools in the compiler).