Description
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 summon
ed:
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.
Activity