higherkindness/mu-scala

Start thinking about Scala 3

cb372 opened this issue · 1 comments

cb372 commented

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 commented

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