Abhijit Sarkar
Abhijit Sarkar

Reputation: 24518

Scala Type: How to Restrict the Generic Type of a Subclass?

I've a trait:

trait OAuthService {
  def sendWithAuthorizationQueryParams[A](request: OAuthRequest)(implicit unmarshaller: Unmarshaller[ResponseEntity, A]): Future[A] = {
    val httpRequest = request.toHttpRequestWithAuthorizationQueryParams

    sendAndReceive(httpRequest, request.signature)
  }

  def sendWithAuthorizationHeader[A](request: OAuthRequest)(implicit unmarshaller: Unmarshaller[ResponseEntity, A]): Future[A] = {
    val httpRequest = request.toHttpRequestWithAuthorizationHeader

    sendAndReceive(httpRequest, request.signature)
  }

  protected def sendAndReceive[A](httpRequest: HttpRequest, id: String)(implicit unmarshaller: Unmarshaller[ResponseEntity, A]): Future[A]
}

I'm creating a subclass:

class StreamingOAuthService()(implicit val actorPlumbing: ActorPlumbing) extends OAuthService {
  private val log = LoggerFactory.getLogger(getClass())

  override protected def sendAndReceive[A](httpRequest: HttpRequest, id: String)(implicit unmarshaller: Unmarshaller[ResponseEntity, A]) = {
    log.debug(s"Http request: {}.", httpRequest)

    import actorPlumbing._

    val host = httpRequest.uri.authority.host.address()

    val connectionFlow: Flow[HttpRequest, HttpResponse, Future[OutgoingConnection]] = Http().outgoingConnectionTls(host)

    Source.single(httpRequest)
      .via(connectionFlow)
      .runWith(Sink.head)
  }
}

In StreamingOAuthService, I want to freeze the generic type as ResponseEntity. In other words, I want to specify that the only type supported by the methods of StreamingOAuthService is ResponseEntity. As shown, StreamingOAuthService.sendAndReceive doesn't compile because the return type is Future[ResponseEntity] and not Future[A], as specified by the trait.

Upvotes: 2

Views: 2496

Answers (3)

Shadowlands
Shadowlands

Reputation: 15074

I thought a bit more about my earlier answer and even in the multi-typed form it still isn't very satisfactory, as you need to define all the types A, B, etc for any instance of the base class you want to use, and that subclass is then only able to accept those types for each method.

Type operators (generalized type constraints) look like they provide a better option:

trait Base {

  type T

  def fun1[A](input: String)(implicit tp: A <:< T): A

  def fun2[A](input: Int)(implicit tp: A <:< T): A
}

class RestrictedSub extends Base {

  override type T = Double

  def fun1[A](input: String)(implicit tp: A <:< T): A = {
    ...
  }

  def fun2[A](input: Int)(implicit tp: A <:< T): A = {
    ...
  }
}

For any call of a method, the compiler can provide a suitable implicit <:<[A,T] (typically written A <:< T, analogous to a binary operator) if and only if A is a subtype of T, so any inappropriate calls should be disallowed at compile time.

For an unrestricted sub-class (a good candidate for a factory method in the trait's companion object), the type T can be set to Any or AnyRef as appropriate.

I do note, though, that I haven't tried using this to build a fully-fleshed out version of your trait with implicit Unmarshallers and Future return types, which may complicate a proper solution.

Upvotes: 2

Shadowlands
Shadowlands

Reputation: 15074

I presume you mean that you want the type specification A to be locked to ResponseEntity in the subclass. If so, you could try adding an abstract type member to the trait and subclass:

trait OAuthService {

  type A

  def sendWithAuthorizationQueryParams(request: OAuthRequest)(implicit unmarshaller: Unmarshaller[ResponseEntity, A]): Future[A] = {
    ...
  }

  ...
}

class StreamingOAuthService()(implicit val actorPlumbing: ActorPlumbing) extends OAuthService {
  private val log = LoggerFactory.getLogger(getClass())

  type A = ResponseEntity

  override protected def sendAndReceive(httpRequest: HttpRequest, id: String)(implicit unmarshaller: Unmarshaller[ResponseEntity, ResponseEntity]) = {
    ...
  }

  ...
}

Note that this assumes A is required to be the same type for all methods within an instance, even in the base trait.

If this is not the intent, the above idea could be extended to define a type for each method (although obviously this could get unweildy quite quickly).

Here's a (simplified) example to give you the idea:

trait Base {

  type A
  type B
  ...

  def fun1(input: String): A

  def fun2(input: Int): B

  ...
}

class Sub extends Base {

  type A = Double
  type B = Double
  ...

  def fun1(input: String): A = {
    input.toDouble
  }

  def fun2(input: Int): B = {
    input.toDouble
  }

  ...
}

Upvotes: 0

Nyavro
Nyavro

Reputation: 8866

You can parametrize whole trait with [A] and get rid of parametrizing each method in trait:

trait OAuthService [A]{
  def sendWithAuthorizationQueryParams(request: OAuthRequest)(implicit unmarshaller: Unmarshaller[ResponseEntity, A]): Future[A] = ...
  ...
}

And then restrict StreamingOAuthService to use ResponseEntity:

class StreamingOAuthService()(implicit val actorPlumbing: ActorPlumbing) extends OAuthService[ResponseEntity] {
...
}

Upvotes: 1

Related Questions