2

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.

Abhijit Sarkar
  • 21,927
  • 20
  • 110
  • 219

3 Answers3

2

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.

Shadowlands
  • 14,994
  • 4
  • 45
  • 43
  • It doesn't compile. I tried with and without a small modification from your proposal that I made the trait generic with `type A`, which's really what the `type T` was doing. `trait OAuthService[A] { def sendWithAuthorizationQueryParams[B](request: OAuthRequest)(implicit ev: B <:< A, unmarshaller: Unmarshaller[ResponseEntity, B]): Future[B] = {...}` Among other errors, `ambiguous implicit values: both method $conforms in object Predef of type [A]⇒ <:<[A,A] and value ev of type <:<[B,A] match expected type <:<[B,A]` and `Cannot prove that B <:< A.` – Abhijit Sarkar Sep 30 '15 at 04:57
  • @AbhijitSarkar Hmm, that's a nuisance. Didn't hit that when playing around in the REPL with my cut-down variant. Maybe you can find a way to block out the `Predef` implicit, as per the answer [here](http://stackoverflow.com/questions/5377492/problem-with-implicit-ambiguity-between-my-method-and-conforms-in-predef). – Shadowlands Sep 30 '15 at 05:15
  • I'm beginning to believe that implicits are Scala's achilles heel as generics is Java's. The half-a$$ed Java generics with type erasure requires more workarounds than the actual code that uses it. – Abhijit Sarkar Sep 30 '15 at 05:26
  • @AbhijitSarkar I reckon I've found implicits to be a blessing way more often than a curse, but yeah, there are always likely to be tricky corner-cases when the limits of the language are pushed. – Shadowlands Sep 30 '15 at 05:47
1

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] {
...
}
Nyavro
  • 8,806
  • 2
  • 26
  • 33
0

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
  }

  ...
}
Shadowlands
  • 14,994
  • 4
  • 45
  • 43