scalatypesscala-2.12

Dependent type constraints (?) on trait in Scala


I have the following model:

sealed trait MyRequest {
  type Response <: MyResponse
}

sealed trait MyResponse {
  type Request <: MyRequest
}

case class SayHelloRequest(name: String) extends MyRequest {
  override type Response = SayHelloResponse
}

case class SayHelloResponse(greeting: String) extends MyResponse {
  override type Request= SayHelloRequest
}

...

Is there some way, at the type level, I can enforce that the request/response pair match together? So if SayHelloRequest has a response type SayHelloResponse then SayHelloResponse must have a request type of SayHelloRequest.

Something like:

MyRequest#Response#Request =:= MyRequest

MyResponse#Request#Response =:= MyResponse

Solution

  • Try F-bounds

    sealed trait MyRequest { self =>
      type This >: self.type <: MyRequest { type This = self.This }
      type Response <: MyResponse { type Request = self.This }
    }
    
    sealed trait MyResponse { self =>
      type This >: self.type <: MyResponse { type This = self.This }
      type Request <: MyRequest { type Response = self.This }
    }
    
    case class SayHelloRequest(name: String) extends MyRequest {
      override type This = SayHelloRequest
      override type Response = SayHelloResponse
    }
    
    case class SayHelloResponse(greeting: String) extends MyResponse {
      override type This = SayHelloResponse
      override type Request = SayHelloRequest
    }
    
    implicitly[SayHelloRequest#Response#Request =:= SayHelloRequest]   // compiles
    implicitly[SayHelloResponse#Request#Response =:= SayHelloResponse] // compiles
    implicitly[MyRequest#Response#Request <:< MyRequest#This]          // compiles
    implicitly[MyResponse#Request#Response <:< MyResponse#This]        // compiles
    

    or a type class (and F-bounds)

    // type class
    trait RequestResponse {
      type Request <: MyRequest[Request]
      type Response <: MyResponse[Response]
    }
    object RequestResponse {
      type Req[Rq <: MyRequest[Rq]] = RequestResponse { type Request = Rq }
      type Resp[Rsp <: MyResponse[Rsp]] = RequestResponse { type Response = Rsp }
      type Aux[Rq <: MyRequest[Rq], Rsp <: MyResponse[Rsp]] =
        RequestResponse { type Request = Rq; type Response = Rsp }
      // materializers
      def req[Rq <: MyRequest[Rq]](implicit
        reqResp: RequestResponse.Req[Rq]
      ): RequestResponse.Aux[Rq, reqResp.Response] = reqResp
      def resp[Rsp <: MyResponse[Rsp]](implicit
        reqResp: RequestResponse.Resp[Rsp]
      ): RequestResponse.Aux[reqResp.Request, Rsp] = reqResp
    }
    
    sealed abstract class MyRequest[This <: MyRequest[This]](
      implicit val reqResp: RequestResponse.Req[This]
    ) { self: This =>
      type Response = reqResp.Response
    }
    
    sealed abstract class MyResponse[This <: MyResponse[This]](
      implicit val reqResp: RequestResponse.Resp[This]
    ) { self: This =>
      type Request = reqResp.Request
    }
    
    case class SayHelloRequest(name: String) extends MyRequest[SayHelloRequest]
    
    case class SayHelloResponse(greeting: String) extends MyResponse[SayHelloResponse]
    
    implicit val sayHelloRequestResponse: RequestResponse.Aux[SayHelloRequest, SayHelloResponse] = null
    
    val rr = RequestResponse.req[SayHelloRequest]
    implicitly[rr.Response =:= SayHelloResponse] // compiles
    val rr1 = RequestResponse.resp[SayHelloResponse]
    implicitly[rr1.Request =:= SayHelloRequest]  // compiles
    

    https://tpolecat.github.io/2015/04/29/f-bounds.html