-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Quotes: generate arbitrary trait or class implementations #11685
Comments
We also need to be able to implement traits in order to port cats-tagless to Scala 3. This is blackbox macro territory so it should be a perfectly reasonable use case for Scala 3 macros. |
What are the classes that you are generating? Could you give a small example of the code you would have and what code is generated? |
Example from cats-tagless (where trait InvariantK[Alg[_[_]]] extends Serializable {
def imapK[F[_], G[_]](af: Alg[F])(fk: F ~> G)(gk: G ~> F): Alg[G]
}
trait FunctorK[Alg[_[_]]] extends InvariantK[Alg] {
def mapK[F[_], G[_]](af: Alg[F])(fk: F ~> G): Alg[G]
override def imapK[F[_], G[_]](af: Alg[F])(fk: F ~> G)(gk: G ~> F): Alg[G] = mapK(af)(fk)
}
trait ContravariantK[Alg[_[_]]] extends InvariantK[Alg] {
def contramapK[F[_], G[_]](af: Alg[F])(fk: G ~> F): Alg[G]
override def imapK[F[_], G[_]](af: Alg[F])(fk: F ~> G)(gk: G ~> F): Alg[G] = contramapK(af)(gk)
}
final case class User(name: String, age: Int)
trait UserRepo[F[_]] {
type Id
def create(user: User): F[Id]
def findAll(olderThan: Int): Stream[F, User]
def pickOne(ids: Id*)(f: List ~> F): F[User]
}
val functorK: FunctorK[UserRepo] = Derive.functorK
// macro generated
new FunctorK[UserRepo] {
override def mapK[F[_], G[_]](af: UserRepo[F])(fk: F ~> G): UserRepo[G] = new UserRepo[G] {
type Id = af.Id
override def create(user: User): F[Id] =
fk(af.create(user))
override def findAll(olderThan: Int): Stream[F, User] =
FunctorK[Stream[*[_], User]].mapK(af.findAll(olderThan))(fk)
override def pickOne(ids: Id*)(f: FunctionK[List, G]): F[User] =
fk(af.pickOne(ids: _*)(ContravariantK[FunctionK[List, *[_]]].contramapK(f)(fk)))
}
} Sorry, I think I messed up |
Good, it seems that you can create that class in a much simpler way. As you seem to know that you want to implement an instance of Here is the basic idea def asRaw[RealRpc](realRpc: RealRpc): RawRpc = ${ asRawExpr('realRpc) }
private def asRaw[RealRpc: Type](realRpc: Expr[RealRpc])(using Quotes): Expr[RawRpc] =
'{
new RawRpc {
def invoke(methodName: String, parameters: Map[String, Json]): Future[Json] =
${invokeExpr(realRpc, 'methodName, 'parameters) }
}
}
private def invokeExpr[RealRpc: Type](realRpc: Expr[RealRpc], methodName: Expr[String], parameters: Expr[Map[String, Json]])(using Quotes): Expr[RawRpc] =
import quotes.reflect._
...
|
Note: we need to implement both an instance of |
Then the second might be problematic. I will have to have a deeper look. |
Coming back to my example with trait JsonCodec[T] {
def encode(value: T): Json
def decode(json: Json): T
}
object JsonCodec {
def apply[T](implicit codec: JsonCodec[T]): JsonCodec[T] = codec
} Then let's assume an example user RPC trait: trait MyRpc {
def handleStuff(foo: String, bar: Int): Future[Boolean]
def doSomethingElse(baz: Double): Future[Unit]
} An invocation of new MyRpc {
def handleStuff(foo: String, bar: Int): Future[Boolean] =
rawRpc.invoke("handleStuff", Map(
"foo" -> JsonCodec[String].encode(foo),
"bar" -> JsonCodec[Int].encode(bar)
)).map(JsonCodec[Boolean].decode)
def doSomethingElse(baz: Double): Future[Unit] =
rawRpc.invoke("doSomethingElse", Map(
"baz" -> JsonCodec[Double].encode(baz)
)).map(JsonCodec[Unit].decode)
} Conversely, an invocation of new RawRpc {
def invoke(methodName: String, parameters: Map[String, Json]): Future[Json] = methodName match {
case "handleStuff" => realRpc.handleStuff(
JsonCodec[String].decode(parameters("foo")),
JsonCodec[Int].decode(parameters("bar"))
).map(JsonCodec[Boolean].encode)
case "doSomethingElse" => realRpc.doSomethingElse(
JsonCodec[Double].decode(parameters("baz"))
).map(JsonCodec[Unit].encode)
}
} Of course, the
|
Unfortunately, it is not something we can realistically have by 3.0. We aim to have this in 3.1. Meanwhile, we could see if there is a possible workaround. It looks like we could use a |
I see, thank you. |
Javascript has a similar concept to Java's Proxy |
Could that work in this instance? |
In my case I will most likely wait for this issue to be resolved rather than use dynamic proxies of any kind. |
Same here, proxy doesn't cut it because we want to modify the parameters / result type |
Out of curiosity - why is it difficult to implement def copy(original: Tree)(name: String, constr: DefDef, parents: List[Tree /* Term | TypeTree */], derived: List[TypeTree], selfOpt: Option[ValDef], body: List[Statement]): ClassDef I was wondering if I could emulate |
👍 for having this, only learned just now that I can't upgrade to Scala 3 because cats-tagless is not available |
I am in need for a solution too and ended up trying this copy workaround. It partly works, as you can then create the ClassDef, but I have an issue referencing its Symbol. First, I am creating/copying a classDef: val Inlined(_, _, Block((otherClass: ClassDef) :: _, _)) = '{class Foo}.asTerm
val classDef = ClassDef.copy(otherClass)("Bar", otherClass.constructor, List(TypeTree.of[T]), None, Nil) // I also tried using "Foo" as a name here as well Then, I define this class and try to instantiate it with Block(
List(classDef),
Apply(Select.unique(New(TypeIdent(classDef.symbol)), "<init>"), Nil)
).asExpr That does nearly create what I want, except that The generated code looks like this and it compiles - just instantiating the wrong class: {
class Bar() extends test.Command
new Foo()
} I have also tried to create Is there any way to construct a |
I would like to help, implementing this feature. Are there any pointers or things that I should look out for? |
The autowire approach should be more doable, because you don't have you actually create instances of the trait. The macro rewrites calls to methods of the trait. The downside is that it's more verbose -- you have to tack on .call everywhere, and the macro is invoked everywhere, so the de/serializers need to be in scope at all the call sites. |
Another approach, which is verbose but mechanical, is you provide a trait with a macro method called impl. Then the user has to extend the trait explicitly, but the right hand side of every method is simply the word It's very quick for users to create the boilerplate - they extend their trait and the impl trait (or perhaps the impl macro is imported not mixed in), then either the IDE generates method stubs with a right hand side of ??? or you copy paste the compiler output which gives you those stubs, then you find/replace ??? with impl. It's straightforward to codegen that too of course. Definitely much less pleasant for the user but I think it's better than being blocked from upgrading. |
A different idea for library design that's also suboptimal but possible with Scala 3 is instead of using a trait with methods and inheritance, you use a case class whose fields are functions. Unfortunately function parameters can't have names. Anyway that replaces the trait with abstract methods, then the server implementation is an instance of the case class with function values filled out rather than a subclass of the trait with the methods implemented, and then the client macro doesn't have to subclass anything, it just has to build an instance of the case class. |
Actually they can, unless I'm misunderstanding what you mean. Example: scala> val f: [A] => (x: A, y: A) => (A, A) = [A] => (_: A) -> (_: A)
val f: [A] => (A, A) => (A, A) = <function2>
scala> f(y = 1, x = 2)
val res0: (Int, Int) = (2,1) |
Oh, great. Here's another approach: https://github.com/propensive/polyvinyl |
My company is interested in sponsoring a production-worthy and well-thought out implementation of this feature. |
amazing, thank you @nicolasstucki ! when do you expect this will be released? |
We try to not rush stabilizing APIs since we're bound by binary compatibility constraints afterwards, but one thing that would help would be to get feedback from users on whether the APIs implemented in #14124 actually solve their problems (see the PR changes for documentation and test cases). To try it out, just set your scalaVersion to a recent nightly to have access to experimental APIs (scroll to the bottom of https://repo1.maven.org/maven2/org/scala-lang/scala3-compiler_3/ to find the version number), e.g. scalaVersion := "3.2.0-RC1-bin-20220419-ef16034-NIGHTLY" |
@smarter I am currently trying to use this feature. So far, it generally works :) But I have one issue right now. I want to create an instance that implements a given trait. So I collect all applicable members of the trait and implement them through some arranged protocol. Right now, I am just not able to programmatically create a new MethodSymbol for another class from a given MethodSymbol. Example: val tree = TypeTree.of[T]
val methods = tree.symbol.memberMethods
def decls(cls: Symbol): List[Symbol] = methods.map { method =>
// just reusing the method here does not work, I guess, because the owner is wrong. So try to create a new MethodSymbol.
Symbol.newMethod(cls, method.name, ???) // How to reuse the type definition of the given method? I failed to build `MethodType` myself from the information available. My tries have all ended up in assertion errors that were difficult to understand.
}
I would then create the How can I do this correctly? Any help would be appreciated. |
@cornerman could you share a self contained minimal example that shows the failure you are getting. The assertion failure on its own might also be helpful insight. One thing that you might be missing is the override flag. |
@nicolasstucki Sure, I can. Actually I think, I have fixed the main issue by using the Is this generally the correct approach? There is still some TODOs in the code:
|
It seems we are missing the I created #15024 adding this new method as Also, to create a nullary method you have to give it a |
Awesome. Thank you! And the |
@ghik was this issue solved for you? I notice it is closed but was not quite sure. |
The problem
Using Scala 2 macros, we have built an RPC framework that can automatically generate implementations of arbitrary traits. These implementations are effectively network proxies that translate every method call into a network request. When the request is received by the RPC server, a reverse translation is performed and a real implementation of this trait is invoked.
A very simplified example of this mechanism:
The
asReal
macro needs to do the following:RealRpc
Json
Json
(assuming that the result type is aFuture
)rawRpc
The
asRaw
macro needs similar capabilities in order to perform a reverse translation.Situation in Scala 3
It's unclear to me whether something like this is possible with Scala 3. Looking at the
Quotes
API, it should in principle be possible if I was able to generate arbitrary trees, starting withClassDef
. However, it seems that currentlyClassDef.apply
is commented out and marked as TODO.So my questions are:
Quotes
API?ClassDef.apply
from being exposed?(Note: I asked this question briefly on Gitter and it was recommended to me to create an issue.)
The text was updated successfully, but these errors were encountered: