Skip to content
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

Allow to implement abstract inline methods for type classes in quotes #22516

Open
goshacodes opened this issue Feb 4, 2025 · 4 comments
Open
Labels

Comments

@goshacodes
Copy link

goshacodes commented Feb 4, 2025

Hi! I want to implement abstract inline method in macro and it fails. The only way I can derive this is via quotation (there are some compile time configuration, which properly can be used only in macro), so it would be really great to allow this.

Compiler version

3.3.5 and 3.6.3

Minimized code

trait TokenWriter

trait JsonWriter[A]:
 inline def write(value: A, tokenWriter: TokenWriter): Unit

object JsonWriter:
 inline def derived[A]: JsonWriter[A] = ${ derivedMacro[A] }

def derivedMacro[A](using quotes: scala.quoted.Quotes): scala.quoted.Expr[JsonWriter[A]] =
  import quotes.reflect.*
 '{
     new JsonWriter[A]:
       inline def write(value: A, tokenWriter: TokenWriter): Unit = ()
  }

Output

[error] 308 |          inline def write(value: T, tokenWriter: TokenWriter): Unit =
[error]     |                     ^
[error]     |                     inline def cannot be within quotes

Expectation

Compiles

@goshacodes goshacodes added itype:bug stat:needs triage Every issue needs to have an "area" and "itype" label labels Feb 4, 2025
@prolativ prolativ added itype:language enhancement area:metaprogramming:quotes Issues related to quotes and splices and removed itype:bug labels Feb 5, 2025
@prolativ
Copy link
Contributor

prolativ commented Feb 5, 2025

Macros have some known limitations originating from their design. Making this work should be treated as a new feature in the language rather than a bug fix, even though I'm not sure if this would be doable at all (or with a justifiable cost).
However, I believe there should be some workaround for your use case, but you would have to describe it with more details.
If all you need is to pass some compile time values to the implementation of a type class instance, make write a non-inline method and inject the config to the implementation like this

import scala.quoted.*

trait TokenWriter

trait JsonWriter[A]:
  def write(value: A, tokenWriter: TokenWriter): Unit

object JsonWriter:
  inline def derived[A]: JsonWriter[A] = ${ derivedMacro[A] }

def derivedMacro[A : Type](using quotes: Quotes): Expr[JsonWriter[A]] =
  val compileTimeConfig: String = "Value from compile time" // fetch compile-time configuration instead
  val configExpr = Expr(compileTimeConfig)
  '{
    new JsonWriter[A]:
      def write(value: A, tokenWriter: TokenWriter): Unit =
        val config: String = ${configExpr}
        println(s"Using config: $config")
        // Serialize A here using config
  }

@goshacodes
Copy link
Author

goshacodes commented Feb 5, 2025

Thank you. My goal is to add an inline to that method since it will really improve runtime performance. So I definitely not want to remove an inline. If my case were simple enough, I would do it without quotes, but I can't.

This is the actual code:
https://github.com/tethys-json/tethys/blob/master/modules/core/src/main/scala-3/tethys/derivation/Derivation.scala#L113-L216

@prolativ
Copy link
Contributor

prolativ commented Feb 5, 2025

Some high-level description of what you need to optimize would still be useful, so that I could advise something.
But maybe you don't really need type classes actually at all? Like instead of

summon[JsonWriter[A]].write(...)

you should do something like

writeJson[A](...)

with writeJson being itself an inline method whose implementation recursively computes Exprs corresponding to about-to-be-inlined implementations of JsonWriter[B]#write (given that B is one of the types referenced by A for which the writer's logic has to be derived) and just then at the very end splice your combined implementation inside writeJson macro

@goshacodes
Copy link
Author

goshacodes commented Feb 5, 2025

We thought about solution without type classes, but we want it to be configurable using type classes as it is now

Consider following models:

case class Foo(x: Int, bar: String) derives JsonWriter

case class Baz(y: Boolean, foo: Foo) derives JsonWriter

Now for Baz it generates

new JsonObjectWriter[Baz]:
  def write(baz: Baz, out: TokenWriter): Unit =
    out.writeObjectStart()
    out.writeFieldName("y")
    summon[JsonWriter[Boolean].write(baz.y, out) // which calls out.writeBoolean(baz.y)
    out.writeFieldName("foo")
    summon[JsonWriter[Foo]].write(baz.foo, out) // which calls some methods on out
    out.writeObjectEnd()

I want it to generate:

new JsonObjectWriter[Baz]:
  def write(baz: Baz, out: TokenWriter): Unit =
     out.writeObjectStart()
     out.writeFieldName("y")
     out.writeBoolean(baz.y)
     out.writeFieldName("foo")
     out.writeObjectStart()
     out.writeFieldName("x")
     out.writeNumber(baz.foo.x)
     out.writeFieldName("bar")
     out.writeString(baz.foo.bar)
     out.writeObjectEnd()
     out.writeObjectEnd()

Or even just:

  out.writeObjectStart()
  out.writeFieldName("y")
  out.writeBoolean(baz.y)
  out.writeFieldName("foo")
  out.writeObjectStart()
  out.writeFieldName("x")
  out.writeNumber(baz.foo.x)
  out.writeFieldName("bar")
  out.writeString(baz.foo.bar)
  out.writeObjectEnd()
  out.writeObjectEnd()

Where final inlined method is this one:

extension[A] (a: A)
  def writeJson(out: TokenWriter)(using jsonWriter: JsonWriter[A]): Unit =
    jsonWriter.write(a, tokenWriter)

So expansion should depend on type class instance method implementation

@prolativ prolativ removed the stat:needs triage Every issue needs to have an "area" and "itype" label label Feb 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants