Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Given the following classes:

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

How do I define in a typesafe manner some function:

def process(req: ???): ???

Such that the following should hold true:

val r1: AddResponse = process(AddRequest(2, 3))
val r2: ToUppercaseResponse = process(ToUppercaseRequest("aaa"))

Also, the following should not compile:

val r3 = process("somestring")
like image 478
jvliwanag Avatar asked Mar 30 '16 13:03

jvliwanag


1 Answers

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.

like image 93
Travis Brown Avatar answered Oct 15 '22 14:10

Travis Brown