How to define a function whose output type depends on the input type

前端 未结 2 779
被撕碎了的回忆
被撕碎了的回忆 2021-02-01 21:18

Given the following classes:

case class AddRequest(x: Int, y: Int)
case class AddResponse(sum: Int)
case class ToUppercaseRequest(str: String)
case class ToUpper         


        
相关标签:
2条回答
  • 2021-02-01 21:36

    This is both entirely possible and a totally reasonable thing to do in Scala. This kind of thing is all over Shapeless, for example, and something similar (but less principled) is the basis of the magnet pattern that shows up in Spray, etc.

    Update: note that the following solution assumes that "given the following classes" means you don't want to touch the case classes themselves. If you don't care, see the second part of the answer below.

    You'd want a type class that maps input types to output types:

    case class AddRequest(x: Int, y: Int)
    case class AddResponse(sum: Int)
    case class ToUppercaseRequest(str: String)
    case class ToUppercaseResponse(upper: String)
    
    trait Processable[In] {
      type Out
      def apply(in: In): Out
    }
    

    And then some type class instances:

    object Processable {
      type Aux[I, O] = Processable[I] { type Out = O }
    
      implicit val toUppercase: Aux[ToUppercaseRequest, ToUppercaseResponse] =
        new Processable[ToUppercaseRequest] {
          type Out = ToUppercaseResponse
          def apply(in: ToUppercaseRequest): ToUppercaseResponse =
            ToUppercaseResponse(in.str.toUpperCase)
        }
    
      implicit val add: Aux[AddRequest, AddResponse] =
        new Processable[AddRequest] {
          type Out = AddResponse
          def apply(in: AddRequest): AddResponse = AddResponse(in.x + in.y)
        }
    }
    

    And now you can define process using this type class:

    def process[I](in: I)(implicit p: Processable[I]): p.Out = p(in)
    

    Which works as desired (note the appropriate static types):

    scala> val res: ToUppercaseResponse = process(ToUppercaseRequest("foo"))
    res: ToUppercaseResponse = ToUppercaseResponse(FOO)
    
    scala> val res: AddResponse = process(AddRequest(0, 1))
    res: AddResponse = AddResponse(1)
    

    But it doesn't work on arbitrary types:

    scala> process("whatever")
    <console>:14: error: could not find implicit value for parameter p: Processable[String]
           process("whatever")
                  ^
    

    You don't even have to use a path dependent type (you should be able just to have two type parameters on the type class), but it makes using process a little nicer if e.g. you have to provide the type parameter explicitly.


    Update: everything above assumes that you don't want to change your case class signatures (which definitely isn't necessary). If you are willing to change them, though, you can do this a little more concisely:

    trait Input[Out] {
      def computed: Out
    }
    
    case class AddRequest(x: Int, y: Int) extends Input[AddResponse] {
      def computed: AddResponse = AddResponse(x + y)
    }
    case class AddResponse(sum: Int)
    
    case class ToUppercaseRequest(str: String) extends Input[ToUppercaseResponse] {
      def computed: ToUppercaseResponse = ToUppercaseResponse(str.toUpperCase)
    }
    case class ToUppercaseResponse(upper: String)
    
    def process[O](in: Input[O]): O = in.computed
    

    And then:

    scala> process(AddRequest(0, 1))
    res9: AddResponse = AddResponse(1)
    
    scala> process(ToUppercaseRequest("foo"))
    res10: ToUppercaseResponse = ToUppercaseResponse(FOO)
    

    Which kind of polymorphism (parametric or ad-hoc) you should prefer is entirely up to you. If you want to be able to describe a mapping between arbitrary types, use a type class. If you don't care, or actively don't want this operation to be available for arbitrary types, using subtyping.

    0 讨论(0)
  • 2021-02-01 21:40

    You can define a common trait for Requests, and a common trait for Responses where the request type is defined for specific response type:

    trait Request[R <: Response]
    trait Response
    
    case class AddRequest(x: Int, y: Int) extends Request[AddResponse]
    case class AddResponse(sum: Int) extends Response
    case class ToUppercaseRequest(str: String) extends Request[ToUppercaseResponse]
    case class ToUppercaseResponse(upper: String) extends Response Response[ToUppercaseRequest]
    

    Then, process signature would be:

    def process[A <: Request[B], B <: Response](req: A): B
    

    When you call process, you'll have to explicitly define the types so that the returned type is what you expect it to be - it can't be inferred specifically enough:

    val r1: AddResponse = process[AddRequest, AddResponse](AddRequest(2, 3))
    val r2: ToUppercaseResponse = process[ToUppercaseRequest, ToUppercaseResponse](ToUppercaseRequest("aaa"))
    
    0 讨论(0)
提交回复
热议问题