Scala案例类构造函数参数的类型是否可以被引用?

3

我的目标是创建一个特质(trait),让case class可以继承它并处理每个构造函数参数,然后将它们作为该case class的方法的参数传递。所有构造函数参数将具有相同的类型,但不同的类型参数,并且该方法将接受与每个构造函数参数的类型参数匹配的参数。您可以将其视为构造函数参数的预处理器。例如:

case class Foo(input1:Input[Int], input2:Input[String]) extends MagicTrait {
  def run(input1:Int, input2:String) { ... }
}

这可行吗?有没有什么不太丑陋的方法(比如完全使用反射)?是否可能以通用方式引用case class的伴生对象(例如,接受Companion.unapply()的输出的函数)?


1
给定类型为Input[T]的值,我如何得到相应的类型为T的值? - Miles Sabin
我知道一种方法,可以从trait截取构造函数。也许有更好的方法来实现你想要的功能。我假设你想创建MagicTrait,因为你不想在每个类中重复这个预处理代码。你想对这些构造函数参数进行什么样的预处理? - Udayakumar Rayala
我的情况可以很好地类比为每个参数都是一个函数,我想运行每个函数,然后使用每个函数的输出调用一个新函数。 - Josh Marcus
1
那么对于当前的目的而言,Input[T]可以被视为等同于() => T吗? - Miles Sabin
“preprocessing”功能是否必须混合到实例中,或者可以由案例类伴随对象提供?如果是这样,那么run的签名是否可以替换Foo的构造函数签名? - Miles Sabin
是的,对于这个讨论,Input[T]可以等同于() => T。我不确定我是否理解——如果伴生对象能够扩展提供一般化处理功能方法的特质,那么“预处理”功能肯定可以由案例类伴生对象提供。这样有帮助吗?你在想什么? - Josh Marcus
2个回答

8

考虑到一个可接受的解决方案允许将预处理功能移动到相关对象,主要剩余困难在于您希望能够抽象出情况类构造函数的元数和类型(即形状)。这可以通过HList实现和来自shapeless的多态函数值实现。

首先是一些准备工作,

import shapeless.HList._
import shapeless.Functions._
import shapeless.Poly._
import shapeless.TypeOperators._

// Implementation of your Input wrapper
case class Input[T](value: T)

// Value extractor as a shapeless polymorphic function value
object value extends (Input ~> Id) {
  def default[T](i : Input[T]) = i.value
}

我们现在可以定义一个预处理器基类,它提供了一个apply方法,该方法接受一组Input类型的HList,将多态函数value映射到其中(即执行预处理),然后将生成的非Input类型的HList传递给提供的案例类构造函数(以hlisted形式给出,请参见下文)。
// Pre-processer base class
abstract class Preprocessor[In <: HList, Out <: HList, R](ctor : Out => R)
  (implicit mapper : MapperAux[value.type, In, Out]) {
    def apply(in : In) = ctor(in map value)
  }

现在,我们定义一个带有后处理组件类型的案例类。
case class Foo(input1 : Int, input2 : String)

并且添加一行样板代码。

object FooBuilder extends Preprocessor((Foo.apply _).hlisted)

现在我们可以使用FooBuilder构建Foo实例了,其中Preprocessor构造函数参数以HListed形式作为所需的补充对象工厂方法提供。

val foo = FooBuilder(Input(23) :: Input("foo") :: HNil)

很遗憾,目前无法将FooBuilder对象与Foo伴生对象组合使用:如果您尝试让Foo伴生对象扩展Preprocessor,您会发现Foo工厂方法不可用作Preprocessor构造函数参数。
为了说明该解决方案确实在类型和元数上进行了抽象,以下是如何添加第二个形状不同的case class的示例。
case class Bar(input1 : Int, input2 : String, input3 : Boolean)

object BarBuilder extends Preprocessor((Bar.apply _).hlisted)

val bar = BarBuilder(Input(23) :: Input("foo") :: Input(true) :: HNil)

1
这是一个很棒的答案!非常感谢。它直接解决了我的问题,非常完整,并且我从中学到了很多内容,并审查了您的shapeless库。 - Josh Marcus

2
case class Input[T](value: T)

trait MagicTrait[T,U] {
   val input1: Input[T]
   val input2: Input[U]
   def run: Unit
}

case class Foo(input1: Input[Int], input2: Input[String]) 
    extends MagicTrait[Int, String] {
  def run = println(input1.value * 2 + input2.value.toUpperCase)
}

scala> val m: MagicTrait[_,_] = Foo(Input(3), Input("hi"))
m: MagicTrait[_, _] = Foo(Input(3),Input(hi))

scala> m.run
6HI

编辑:

如果您想找到类参数的类型,可以使用案例类扩展Product的事实:

scala> Foo(2, "hi").productIterator.map(_.asInstanceOf[AnyRef].getClass).toList
res13: List[java.lang.Class[_]] = 
         List(class java.lang.Integer, class java.lang.String)

但这会使用你想要避免的反射。这就是为什么我们使用参数化的原因。

如果你想要返回其伴随对象,我不确定在 case classes 的上下文中你能否以有用、类型安全的方式做到这一点,因为伴随对象没有扩展指定其提取器方法的接口。你可以尝试使用结构类型来解决问题,但很可能有更好的方法来解决你试图解决的任何问题。


我的想法是在不必将特质参数化为案例类参数类型的情况下引用案例类参数的类型。如果我要按照这个思路操作,我需要为每个元数创建一个特质,比如 MagicTrait1(一个参数),MagicTrait2(两个参数),MagicTrait3 等等。但也许没有更好的解决方案? - Josh Marcus
这里的Input[_]类型构造器发生了什么? - Miles Sabin
@MilesSabin,我已经将其添加到我的答案中,但除了让事情变得混乱之外,它并没有改变任何东西;使用case类构造函数参数实现带有参数的trait的要点是相同的。 - Luigi Plinge

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接