Scala:如何在case类构造函数中使用类型作为第一类值?

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Scala:如何在case类构造函数中使用类型作为第一类值?相关的知识,希望对你有一定的参考价值。

假设我有几个自动生成的类,如MyEnum1MyEnum2,...(它们不一定是Scala枚举类型,只是一些自动生成的类)。虽然MyEnum1的类型不同于MyEnum2的类型(并且它们除了Any之外它们不共享自动生成的父类型),但我可以保证所有这些自动生成的类型具有完全相同的公共,静态方法,特别是findByIdfindByName,它允许根据索引或字符串名称查找枚举值。

我正在尝试创建一个函数,它将使用findByIdfindByName的类型特定版本,但是通用接受任何MyEnum1MyEnum2,...作为函数参数。

请注意,从不同的枚举中创建一个sum类型的典型sealed trait + case class模式在这里没有用,因为我在谈论基于类型参数调度不同的静态方法,并且根本不涉及任何实际的值参数。

例如,假设MyEnum1编码男/女性别。所以MyEnum1.findById(0)返回MyEnum1.Female类型MyEnum1。并且说MyEnum2编码眼睛颜色,因此MyEnum2.findById(0)返回MyEnum2.Green类型MyEnum2

我得到一个Map,其中键是类型,值是要查找的索引,例如

val typeMap = Map(
  MyEnum1 -> 0,
  MyEnum2 -> 0
)

我想一般这样做:

for ( (elemType, idx) <- typeMap ) yield elemType.findById(v)
                                         |---------------|
                                          the goal is to
                                          avoid boilerplate
                                          of defining this
                                          with different
                                          pattern matching
                                          for every enum.

并返回一些看起来像的序列类型(可以有元素类型Any

MyEnum1.Female, MyEnum2.Green, ...

我已经在sealed trait + case class样板上挣扎了一段时间,它似乎在概念上并不正确。无论我将MyEnum1MyEnum2的值包装到像FromMyEnum1(e: MyEnum1)这样的case类值构造函数中并尝试定义操作该值的implicits,当我想要执行elemType.findById(...)时,它在上面的代码示例中没有帮助,因为编译器仍然说那种类型Any(它解决了我的Map中的关键类型),没有方法findById

我非常希望不将类型本身包装在一个案例类模式中作为键,但我可以这样做 - 除了我无法看到如何将类型本身视为案例类中的第一类值构造函数,天真的样子

case class FromMyEnum1(e: MyEnum1.getClass) extends EnumTrait

(这样Map键可以有类型EnumTrait,并且可能有一些隐含的,将每个case类构造函数与findByIdfindByName的正确实现相匹配)。

任何有助于理解Scala如何使用类型本身作为case类值构造函数内部值的帮助将不胜感激!

答案

你的问题有一些根本的误解。

首先,Scala中没有“静态方法”,所有方法都附加到类的实例。如果您想要一个类的每个实例都相同的方法,您可以将方法添加到该类的伴随对象并在该对象上调用它。

其次,你不能在一个类型上调用一个方法,你只能在一个类型的实例上调用一个方法。所以你不能在你的一个findById类型上调用MyEnum,你只能在其中一种类型的实例上调用它。

第三,您不能从方法返回类型,您只能返回类型的实例。


很难确切地说出你想要实现的目标,但我怀疑MyEnum1MyEnum2应该是对象,而不是类。这些继承自您定义的通用接口(findByIdfindByName)。然后,您可以从常用类型的实例创建Map,以在findById调用中使用该索引。


示例代码:

trait MyEnum {
  def findById(id: Int): Any
  def findByName(name: String): Any
}

object MyEnum1 extends MyEnum {
  trait Gender
  object Male extends Gender
  object Female extends Gender

  def findById(id: Int): Gender = Male
  def findByName(name: String): Gender = Female
}

object MyEnum2 extends MyEnum {
  trait Colour
  object Red extends Colour
  object Green extends Colour
  object Blue extends Colour

  def findById(id: Int): Colour = Red
  def findByName(name: String): Colour = Blue
}

val typeMap = Map(
  MyEnum1 -> 0,
  MyEnum2 -> 0,
)


for ((elemType, idx) <- typeMap ) yield elemType.findById(idx)

如果您无法提供常见的父级trait,请使用结构类型:

object MyEnum1 {
  trait Gender
  object Male extends Gender
  object Female extends Gender

  def findById(id: Int): Gender = Male
  def findByName(name: String): Gender = Female
}

object MyEnum2 {
  trait Colour
  object Red extends Colour
  object Green extends Colour
  object Blue extends Colour

  def findById(id: Int): Colour = Red
  def findByName(name: String): Colour = Blue
}

type MyEnum = {
  def findById(id: Int): Any
  def findByName(name: String): Any
}

val typeMap = Map[MyEnum, Int](
  MyEnum1 -> 0,
  MyEnum2 -> 0,
)

for ((elemType, idx) <- typeMap) yield elemType.findById(idx)
另一答案

如果类具有实际实例(单例对象计数),则可以使用结构类型:

type Enum[A] = {
  def findById(id: Int): E
  def findByName(name: String): E
  def values(): Array[E]
}

trait SomeEnum
object SomeEnum {
  case object Value1 extends SomeEnum
  case object Value2 extends SomeEnum

  def findById(id: Int): SomeEnum = ???
  def findByName(name: String): SomeEnum = ???
  def values(): Array[SomeEnum] = ???
}

trait SomeEnum2
object SomeEnum2 {
  case object Value1 extends SomeEnum2
  case object Value2 extends SomeEnum2

  def findById(id: Int): SomeEnum2 = ???
  def findByName(name: String): SomeEnum2 = ???
  def values(): Array[SomeEnum2] = ???
}

val x: Enum[SomeEnum] = SomeEnum
val y: Enum[SomeEnum2] = SomeEnum2

因此,如果您只使用Scala,事情就很简单。

但是Java类没有伴随对象 - 你最终会得到object mypackage.MyEnum is not a value。这不行。您必须为此使用反射,因此在所有情况下保持API包含都存在问题。

但是,你可以做的是这样的:

  1. 定义一组共同的操作,例如 trait Enum[A] { def findById(id: Int): A = ??? def findByName(name: String): A = ??? def values(): Array[A] = ??? }
  2. 分离处理每个案例: def buildJavaEnumInstance[E <: java.util.Enum: ClassTag]: Enum[E] = new Enum[E] { // use reflection here to implement methods // you dont } def buildCoproductEnum = // use shapeless or macros to get all known instances // https://stackoverflow.com/questions/12078366/can-i-get-a-compile-time-list-of-all-of-the-case-objects-which-derive-from-a-sea ...
  3. 创建一个伴随对象,并使用implicit处理这些案例: object Enum { def apply[E](implicit e: Enum[E]): Enum[E] = e implicit def buildJavaEnumInstance[E <: java.util.Enum: ClassTag] = ??? implicit def buildCoproductEnum = ??? ... }
  4. 使用Enum作为类型类或其他东西。 def iNeedSomeEnumHere[E: Enum](param: String): E = Enum[E].findByName(param)

我同意,这需要大量的前期编码。对于图书馆来说可能是一个好主意,因为我相信不仅是你有这个问题。

以上是关于Scala:如何在case类构造函数中使用类型作为第一类值?的主要内容,如果未能解决你的问题,请参考以下文章

如何创建类型化工厂方法构造函数的类层次结构并使用抽象类型从 Scala 访问它们?

scala 常用模式匹配类型

大数据进阶之路——Scala 函数和对象

Scala入门到精通——第十五节 Case Class与模式匹配

scala 模式匹配

Scala案例类伴随对象 - 类型名称冲突