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

Start thinking about Scala 3 #894

Open
cb372 opened this issue Apr 27, 2020 · 1 comment
Open

Start thinking about Scala 3 #894

cb372 opened this issue Apr 27, 2020 · 1 comment

Comments

@cb372
Copy link
Member

cb372 commented Apr 27, 2020

Scala 3 is just around the corner ("late 2020"). The macro annotation will not work, so we need to find a new way to generate servers and clients from service definitions.

I played around with Scala 3's generic derivation feature and macros today, and I think it might work for our use case. I couldn't get my code to compile (documentation is almost non-existent!) but I think it can work in theory.

My idea was to define type classes in Mu containing the various server/client factory methods, e.g.

trait RPCClientSide[S[_[_]]] {
  def unsafeClient[F[_]: Async](channelFor: ChannelFor, serializationType: SerializationType): S[F]
}

trait RPCServerSide[S[_[_]]] {
  // serialization type, compression type, other options would also be passed as arguments here
  def bindService[F[_]: Async](service: S[F]): F[ServerServiceDefinition]
}

and implement derived methods for each of them using macros and TASTy reflection:

import scala.quoted._

object RPCClientSide {

  given gen[S[_[_]]: Type](using qctx: QuoteContext) as Expr[RPCClientSide[S]] = {
    import qctx.tasty._

    '{
      new RPCClientSide[S] {
        def unsafeClient[F[_]: Async](channelFor: ChannelFor, serializationType: SerializationType): S[F] =
          throw new Exception("TODO generate the client implementation")
      }
    }
  }

  implicit inline def derived[S[_[_]]]: RPCClient[S] = ${ RPCClient.gen[S] }

}

Then the service definition trait would have a derives clause instead of a @service annotation:

trait MyService[F[_]] derives RPCServerSide, RPCClientSide {
  def sayHello(req: HelloRequest): F[HelloResponse]
}

(We don't have to split the client and server side into separate type classes, it's just an idea.)

The derived type class instance can be summoned:

val channelFor = ...
val instance = summon[RPCClientSide[MyService]]
val client = instance.unsafeClient[IO](channelFor, Protobuf)
client.sayHello(HelloRequest("Chris"))

Another option is to do two stages of source code generation, as explored in #632. But that POC is based on a Scala 2 compiler plugin, so it would need to be rewritten.

@cb372
Copy link
Member Author

cb372 commented May 29, 2020

There's a working proof-of-concept macro here to generate a gRPC service definition from a service instance.

There are also a few slides to explain the idea.

I made a few simplifications for the POC, e.g. the service trait is Greeter, not Greeter[F[_]], and we don't use PBDirect to derive the protobuf marshaller. But I think it's enough to show that this idea would work. I won't do any more work on this for now, because:

  • the dotty metaprogramming features are still quite buggy and under-documented. It's a bit early to start using them for anything proper.
  • we will need dotty cross-builds of cats, cats-effect, PBDirect, avro4s and many other dependencies before we can support dotty in Mu

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

No branches or pull requests

1 participant