如何定义输出类型取决于输入类型的函数

jvl*_*nag 18 types scala shapeless

鉴于以下课程:

case class AddRequest(x: Int, y: Int)
case class AddResponse(sum: Int)
case class ToUppercaseRequest(str: String)
case class ToUppercaseResponse(upper: String)
Run Code Online (Sandbox Code Playgroud)

如何以类型安全的方式定义一些函数:

def process(req: ???): ???
Run Code Online (Sandbox Code Playgroud)

这样以下内容应该成立:

val r1: AddResponse = process(AddRequest(2, 3))
val r2: ToUppercaseResponse = process(ToUppercaseRequest("aaa"))
Run Code Online (Sandbox Code Playgroud)

此外,下面的应该不会编译:

val r3 = process("somestring")
Run Code Online (Sandbox Code Playgroud)

Tra*_*own 28

这在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
}
Run Code Online (Sandbox Code Playgroud)

然后是一些类型类实例:

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)
    }
}
Run Code Online (Sandbox Code Playgroud)

现在您可以process使用此类型类进行定义:

def process[I](in: I)(implicit p: Processable[I]): p.Out = p(in)
Run Code Online (Sandbox Code Playgroud)

哪个工作符合要求(注意适当的静态类型):

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

scala> val res: AddResponse = process(AddRequest(0, 1))
res: AddResponse = AddResponse(1)
Run Code Online (Sandbox Code Playgroud)

但它不适用于任意类型:

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

您甚至不必使用路径依赖类型(您应该只能在类型类上有两个类型参数),但是process如果您必须明确提供类型参数,它会使得使用更好一些.


更新:以上所有内容都假定您不想更改案例类签名(这绝对不是必需的).但是,如果你愿意改变它们,你可以更简洁地做到这一点:

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
Run Code Online (Sandbox Code Playgroud)

然后:

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

scala> process(ToUppercaseRequest("foo"))
res10: ToUppercaseResponse = ToUppercaseResponse(FOO)
Run Code Online (Sandbox Code Playgroud)

您应该选择哪种多态(参数或特殊)完全取决于您.如果您希望能够描述任意类型之间的映射,请使用类型类.如果您不关心,或者主动不希望此操作可用于任意类型,则使用子类型.