【问题标题】:How to do generic tuple -> case class conversion in Scala?如何在Scala中进行通用元组->案例类转换?
【发布时间】:2015-06-13 05:26:05
【问题描述】:

假设有人提供了一个函数:

def getTupleData[T](source: String): List[T] = {
  // ...
}

我需要编写一个函数,它以案例类C 作为类型参数,并在上述函数的帮助下返回List[C]。这是我到目前为止所得到的:

def getCaseClassData[C](source: String): List[C] = {
  // Somehow get T from C.
  // For example, if C is case class MyCaseClass(a: Int, b: Long), then T is (Int, Long)
  // How to get T?      

  getTupleData[T](source) map { tuple: T =>
    // Somehow convert tuple into a case class instance with the case class type parameter
    // C.tupled(tuple) ??  Type parameter cannot be used like this. :(
  }
}

更具体地说,在我看来,我在这里问了两个问题:

  1. 如何从表示案例类的类型参数中显式获取元组的类型,以便将其用作类型参数?
  2. 如何在不知道案例类的实际名称而只知道类型参数的情况下从元组实例创建案例类实例?

【问题讨论】:

  • 这不能回答我的问题:1)我在这里谈论的是泛型,我看到的故事完全不同,2)你提到的问题是从案例类转换为元组。
  • T类型表示什么?
  • 我希望它是对应元组的类型。

标签: scala


【解决方案1】:

您不会找到任何相当简单或直接的方法来做到这一点。如果您准备好接受更多涉及的解决方案,请耐心等待。

每个案例类在其伴随对象中都有一个apply 方法,用于实例化该类。通过在这个方法上调用tupled(在eta-expansion之后),你会得到一个函数,它接受一个元组并创建相应的案例类实例。

现在问题当然是每个案例类的apply 都有不同的签名。我们可以通过引入一个表示案例类工厂的类型类来解决这个问题,并通过宏提供该类型类的实例(它只会委托给案例类的apply 方法)。

import scala.reflect.macros.whitebox.Context
import scala.language.experimental.macros

trait CaseClassFactory[C,T]{
  type Class = C
  type Tuple = T
  def apply(t: Tuple): C
}

object CaseClassFactory {
  implicit def factory1[C,T]: CaseClassFactory[C,T] = macro factoryImpl[C,T]
  implicit def factory2[C]: CaseClassFactory[C,_] = macro factoryImpl[C,Nothing]
  def apply[C,T]: CaseClassFactory[C,T] = macro factoryImpl[C,T]
  def apply[C]: CaseClassFactory[C,_] = macro factoryImpl[C,Nothing]

  def factoryImpl[C:c.WeakTypeTag,T:c.WeakTypeTag](c: Context) = {
    import c.universe._
    val C = weakTypeOf[C]
    val companion = C.typeSymbol.companion match {
      case NoSymbol => c.abort(c.enclosingPosition, s"Instance of $C has no companion object")
      case sym      => sym
    }
    val tupledTree = c.typecheck(q"""($companion.apply _).tupled""")
    val T = tupledTree.tpe match {
      case TypeRef(_, _, List(argTpe, _)) => argTpe
      case t => c.abort(c.enclosingPosition, s"Expecting type constructor (Function1) for $C.tupled, but got $t: ${t.getClass}, ${t.getClass.getInterfaces.mkString(",")}")
    }
    if (! (c.weakTypeOf[T] <:< T)) {
      c.abort(c.enclosingPosition, s"Incompatible tuple type ${c.weakTypeOf[T]}: not a sub type of $T")
    }
    q"""
    new CaseClassFactory[$C,$T] {
      private[this] val tupled = ($companion.apply _).tupled
      def apply(t: Tuple): $C = tupled(t)
    }
    """
  }
}

有了它,你可以做这样的事情:

scala> case class Person(name: String, age: Long)
defined class Person

scala> val f = CaseClassFactory[Person]
f: CaseClassFactory[Person]{type Tuple = (String, Long)} = $anon$1@63adb42c

scala> val x: f.Tuple = ("aze", 123)
x: f.Tuple = (aze,123)

scala> implicitly[f.Tuple =:= (String, Long)]
res3: =:=[f.Tuple,(String, Long)] = <function1>

scala> f(("aze", 123))
res4: Person = Person(aze,123)

但更重要的是,您可以要求 CaseClassFactory 的实例作为隐式参数,从而允许一般地实例化您的案例类。然后,您可以执行以下操作:

scala> implicit class TupleToCaseClassOps[T](val t: T) extends AnyVal {
     |   def toCaseClass[C](implicit f: CaseClassFactory[C,T]): C = {
     |     f(t)
     |   }
     | }
defined class TupleToCaseClassOps

scala> case class Person(name: String, age: Long)
defined class Person

scala> ("john", 21).toCaseClass[Person]
res5: Person = Person(john,21)

相当整洁。有了这个类型类,getCaseClassData 就变成了:

def getCaseClassData[C](source: String)(implicit f: CaseClassFactory[C,_]): List[C] = {
  getTupleData[f.Tuple](source) map { tuple: f.Tuple =>
    f(tuple)
  }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-06-16
    • 2021-06-07
    • 2016-07-27
    相关资源
    最近更新 更多