1

I'm struggling to get information of type in Scala3 macro implementation. I'll explain problem through code.

Here is application logic:

object BlockServiceImpl extends BlockService:
  def authenticateUser0() = new ServiceCall[AuthUser,AuthUserResponse]:
     def invoke(request: AuthUser): Future[AuthUserResponse] = 
       println("BlockServiceImpl authenticateUser0 called")
       Future.successful(AuthUserResponse("test"))   

Now, for the logic I want to make endpoints with help of macro.

defineRoute("POST","/v1/block",BlockServiceImpl.authenticateUser0)

This is inline method:

inline def defineRoute[Q: RootJsonFormat ,R: RootJsonFormat](method: String, uri: String,inline call: () =>  ServiceCall[Q,R]): AkkaHttpCall = ${ methodImpl[Q,R]('uri, 'call)}

And this is implementation of macro:

def methodImpl[Q: Type,R: Type](uri: Expr[String],expr: Expr[Function0[ServiceCall[Q,R]]])(using ctx: Quotes): Expr[AkkaHttpCall] = ...

How can I get information that Q is AuthUser type during macro expansion in a compile time?

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
zlaja
  • 1,351
  • 3
  • 13
  • 23
  • `Q: Type, R: Type` you have no bounds on your `methodImpl`, can you bind them accordingly? (I'm not super familiar with the new macros, but how can you enforce a certain type if the type has no bounds?) – Randomness Slayer May 07 '21 at 00:25
  • How to bind them? – zlaja May 07 '21 at 08:20
  • I think using upper bound : `Q <: AuthUser : Type`. – gianluca aguzzi May 07 '21 at 09:48
  • There will be different defineRoute calls with different Q type. – zlaja May 07 '21 at 10:37
  • 1
    @zlaja Type bounds can be extremely useful if the type hierarchy is well defined. For the same or sub-classes, we can use an "upper bound" or "[T <: R]". For the same or super-classes, we can use a "lower bound" or "[T >: R]". We can bind both ends simultaneously using "[T >: R <: W]". I've used this as a reference in the past: https://blog.knoldus.com/scala-type-bounds/ – Randomness Slayer May 08 '21 at 02:08
  • Considering that several of your base types are not provided, specifically addressing your actual implementation issues are not possible without guessing. "Black-Box Types" AKA "Types that are givens for you but are not necessarily known to me": BlockService, ServiceCall, AuthUser, AuthUserResponse, RootJsonFormat , AkkaHttpCall – Randomness Slayer May 08 '21 at 02:28
  • @RandomnessSlayer, yes I understand the idea, but it can be any case class/object, they are request/response actually. I just wanted to know if user used special cases for request/response (NotUsed, Done, etc). So, that information I put in serializers. Lagom framework implemented it in that way. – zlaja May 11 '21 at 10:29

3 Answers3

2

A possible solution could be to use pattern matching on quoted expressions.

So for example, you can define a method that is used to retrieve the compile-time type:

def tag[A <: AnyKind] = throw new IllegalStateException("use it only to pattern match types")

And then, in the macro expansion, you can perform pattern match as:

'{ tag[Q] } match {
      case '{ tag[AuthUser] } => // here I am sure that Q is AuthUser, since Q is matched with AuthUser
}

It is quite a trick (and is not very extensible as you have to add each type) so take everything I say with a grain of salt... I think that exists a clearer solution depends on your particular application logic :)

gianluca aguzzi
  • 1,734
  • 1
  • 10
  • 22
  • 1
    This could work. I don't need to figure out information for every type but just for one special case. I will try it later. – zlaja May 07 '21 at 10:41
  • 1
    I've solved it differently. I've added information for special type in serializers. So, in a serializer I know if it were NotUsed,Done. – zlaja May 11 '21 at 09:08
  • 1
    Good, why you don't answer the question yourself so, in the future, other people have a possible solution for this problem? :) – gianluca aguzzi May 11 '21 at 09:30
  • I forget it :). – zlaja May 11 '21 at 10:30
1

Bounding (See: Scala 3 Book: Context Bounds) the type of a parameter in a function can be achieved in several ways.


THIS IS WRONG (TY Dmytro!): When using generic parameters like so: [T : Type] we are aliasing a type

CORRECTION: When using generic parameters like so: [T : R] we are using syntactic sugar which represents an implicit parameter of type R[T]


For many applications, including yours, it can be beneficial to restrict the type of our generic parameter.


There are two main bounds, an "upper" and a "lower" bound.

The "upper" bound e.g. [T <: U] specifies that T must be of type U, or a subclass of U

The "lower" bound e.g. [T >: U] specifies that T must be of type U, or a super-class of U

It is possible to restrict both bounds, by first specifying the lower bound then the upper bound, e.g. [T >: Cat <: Animal]

  • 1
    *"When using generic parameters like so: `[T : Type]` we are aliasing a type"* Context bounds do not alias a type. – Dmytro Mitin Sep 26 '22 at 17:59
0

I've solved it by putting information if Q and R were special cases (NotUsed, Done,..) in serializers for Q and R. The idea is took from the Lagom framework.

zlaja
  • 1,351
  • 3
  • 13
  • 23
  • 2
    Your own answer contains zero technical information to help another person solve similar problems. You've basically accepted an answer saying "I've fixed it myself using a framework called Lagom" which, without context, is not helpful to fix a similar but not exactly the same value. Which is the purpose of StackOverflow questions and answers, to be able to understand the why or mechanisms to the question/problem at hand. – Randomness Slayer Jun 03 '21 at 05:29