Skip to content

Seeking Feedback : make quotes.reflect.* top-level definitions #23330

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

Kalin-Rudnicki
Copy link

@Kalin-Rudnicki Kalin-Rudnicki commented Jun 8, 2025

Precursor : The power of the scala 3 macro system is AMAZING. By far the best metaprogramming experience I have had. That being said, there are a few usability issues because of the way the API is defined that makes it hard to work with, that this proposal aims to address.

Problem Statement

As a user of the Quotes API, the nesting of all the types within the Quotes.reflect module makes the API very difficult to use.

Any program of any significance ends up doing something like

final class MyCode(using val quotes: Quotes) {
  // ...
}

This is NOT because its an issue to have a (using Quotes) everywhere, its an issue because all the types are within the quotes object itself. So, unless you have some outer scoped Quotes, you cant do something like:

final case class ConstructorHelper(sym: Symbol) { /* ... */ }

def getConstructor[A: Type](using Quotes): ConstructorHelper = ???

You can get around this by doing something like

final case class ConstructorHelper(quotes: Quotes, sym: quotes.reflect.Symbol) { /* ... */ }

but then the nesting of types will make working with different instances very difficult.

It can be seen in the Quotes.scala file, which is over 5.5k lines long, and this is exactly what projects that end up working with the quotes API end up looking like, huge files, because the structure of the API makes it very difficult, if not sometimes impossible to split into separate files, without having to do all sorts of asInstanceOf in order to get the compiler to comply.

Proposed Solution

package scala.quoted.meta

trait Meta {

  val internal: Meta.Internal

}
object Meta {

  trait Internal {
    val block: BlockAPI
  }

  trait BlockAPI {

      /** Creates a block `{ <statements: List[Statement]>; <expr: Term> }` */
      def apply(stats: List[Statement], expr: Term): Block

      def copy(original: Tree)(stats: List[Statement], expr: Term): Block

  }

}
trait Block private[meta] extends Term {
  override type Self <: Block

  def statements: List[Statement]

  def expr: Term

}
object Block {

  def api(using meta: Meta): Meta.BlockAPI = meta.internal.block
  given Meta => Conversion[Block.type, Meta.BlockAPI] = _.api

  /** Matches a block `{ <statements: List[Statement]>; <expr: Term> }` */
  def unapply(x: Block): (List[Statement], Term) = (x.statements, x.expr)

}

then you can use this at a top level like

def uselessBlock(term: Term)(using Meta): Block =
  Block(Nil, term) // or: Block.api.apply(Nil, term)

The core principle here is that you need an instance of Meta in order to create any of these types, but they are not scoped to an individual instance, like with Quotes.

Potential Pushback

As long as the Quotes API is backwards compatible after such a change, the only downside I can think of with this change is that all types are not scoped to the same exact instance, if that was important, which based on my impression, does not seem to be the case. The only way you can get an instance of Quotes is the entry-point to a macro, so I dont anticipate this being an issue.

Also , when you are quoting and splicing, it is my understanding that those functions are creating nested instances of Quotes. It therefore seems far more detrimental to have an API that encourages use like this:

final class MyCode(using val quotes: Quotes) {

  final case class MyHelper(
    sym: Symbol, // this is using the quotes from the constructor, not from the interpolation in `def show`
  )
  object MyHelper {
    def get[A: Type]: MyHelper = ???
  }

  def stuff[A: Type](expr: Expr[A]): Expr[String] = {
    val helper = MyHelper.get[A]
    ???
  }

  def deriveShow[A:  Type] = '{
    new Show[A] {
      def show(a: A): String = ${ stuff('a) }
    }
  }

}

Ive seen this paradigm used in other helper libs like magnolia, and used it many times in my own projects, and not once has it caused an issue, which tells me that its really not that important that these instances are scoped. It seems to be an unfortunate design decision, with the aim of abstracting away the actual compiler types under the hood. It is entirely possible to achieve this with traits that are not implemented until downstream, but the user gets normal types to work with.

Compatibility

trait Quotes extends Meta {
  @deprecated
  val reflect: reflectModule = ??? // actually implement
}

It seems like it should be possible to deprecate the reflect module, and have all the implementations within it defer to the internal: Meta.Internal functions. I believe this would be considered binary compatible? Also, with having an instance of Quotes at the entry-point of your macro, Quotes is a Meta, and you can therefore easily use the proposed API.

@Kalin-Rudnicki
Copy link
Author

Hello, before I go all the way down the path of making this change, I would like to see if there is an openness to this change.

Looking for input on that.

@Kalin-Rudnicki Kalin-Rudnicki force-pushed the current/refactor/quotes-api branch from f05bc0d to 59d8d02 Compare June 8, 2025 22:15
@Kalin-Rudnicki Kalin-Rudnicki changed the title DRAFT : make quotes.reflect.* top-level definitions Seeking Feedback : make quotes.reflect.* top-level definitions Jun 9, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant