Как определить функцию, тип которой зависит от типа ввода

Учитывая следующие классы:

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

Как определить в виде файла некоторую функцию:

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

Таким образом, должно выполняться следующее:

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

Также не следует компилировать следующее:

val r3 = process("somestring")

Ответы

Ответ 1

Это совершенно возможно и вполне разумно сделать в Scala. Например, это нечто совершенно бесформенное, и что-то подобное (но менее принципиальное) является основой шаблона магнита, который появляется в Spray и т.д.

Обновление: обратите внимание, что в следующем решении предполагается, что "с учетом следующих классов" означает, что вы не хотите касаться самих классов. Если вам все равно, см. Вторую часть ответа ниже.

Вам нужен класс типа, который отображает типы ввода в типы вывода:

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
}

И затем некоторые экземпляры класса типов:

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)
    }
}

И теперь вы можете определить process с помощью этого типа:

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

Что работает по желанию (обратите внимание на соответствующие статические типы):

scala> val res: ToUppercaseResponse = process(ToUppercaseRequest("foo"))
res: ToUppercaseResponse = ToUppercaseResponse(FOO)

scala> val res: AddResponse = process(AddRequest(0, 1))
res: AddResponse = AddResponse(1)

Но это не работает для произвольных типов:

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

Вам даже не нужно использовать тип, зависящий от пути (вы должны иметь возможность иметь только два типа параметров в классе типов), но он делает использование process немного приятнее, если, например, вы должны явно указать параметр типа.


Обновление: все вышеперечисленное предполагает, что вы не хотите менять свои подписи класса case (что определенно не обязательно). Если вы хотите их изменить, вы можете сделать это немного более кратко:

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

И затем:

scala> process(AddRequest(0, 1))
res9: AddResponse = AddResponse(1)

scala> process(ToUppercaseRequest("foo"))
res10: ToUppercaseResponse = ToUppercaseResponse(FOO)

Какой тип полиморфизма (параметрический или ad-hoc), который вы предпочитаете, полностью зависит от вас. Если вы хотите описать сопоставление между произвольными типами, используйте класс типа. Если вам все равно или активно не хотят, чтобы эта операция была доступна для произвольных типов, используйте подтипирование.

Ответ 2

Вы можете определить общий признак для запросов и общий признак для ответов, в которых тип запроса определен для определенного типа ответа:

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]

Тогда сигнатура process будет:

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

Когда вы вызываете process, вам придется явно определять типы, чтобы возвращаемый тип был тем, что вы ожидаете от него, - его нельзя сделать достаточно точно:

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