【问题标题】:Scala: how to use types as first-class values inside of case class constructors?Scala:如何在案例类构造函数中使用类型作为一等值?
【发布时间】:2019-02-06 13:52:03
【问题描述】:

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

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

请注意,用典型的sealed trait + case class 模式从不同的枚举中创建一个 sum 类型在这里没有帮助,因为我说的是基于类型参数调度不同的静态方法,而且从来没有完全涉及实际值参数。

例如,假设MyEnum1 编码男性/女性性别。这样MyEnum1.findById(0) 返回MyEnum1.Female,其类型为MyEnum1。假设MyEnum2 编码眼睛颜色,所以MyEnum2.findById(0) 返回MyEnum2.Green,其类型为MyEnum2

给我一​​个Map,其中key是type,value是要查找的索引,比如

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 样板而苦苦挣扎,从概念上讲,它似乎不是正确的方法。无论我是否将MyEnum1MyEnum2values 包装到FromMyEnum1(e: MyEnum1) 之类的case 类值构造函数中,并尝试定义对那个value 进行操作的隐式,它都不会当我想做elemType.findById(...) 时,在上面的代码示例中没有帮助,因为编译器仍然说Any 类型(它为我的Map 中的键类型解析的内容)没有方法findById

我强烈不希望将类型本身包装在案例类模式中作为键,但我可以这样做——除了我看不出如何将类型本身视为第一类值一个案例类的构造函数,天真的像

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

(因此Map 键可以具有EnumTrait 类型,并且可能有一些隐式将每个案例类构造函数与findByIdfindByName 的正确实现相匹配。

任何帮助理解 Scala 如何将类型本身用作案例类值构造函数中的值将不胜感激!

【问题讨论】:

  • “任何有助于理解 Scala 如何使用类型本身作为案例类值构造函数中的值的帮助将不胜感激!” – 不幸的是,这在 Scala 中根本不可能(事实上几乎所有静态类型的编程语言,除了极少数高度晦涩的研究语言)。在 Scala 中,像大多数静态类型的编程语言一样,typesvalues 的世界是严格分开的。
  • 您可以在 Scala 中轻松看到这一点,您可以同时拥有同名的类型和值(例如 Seq)而不会遇到任何冲突。
  • @JörgWMittag 我认为您的评论不正确。例如,在 Haskell 中,因为您可以通过仅提供它们的实现来将现有类型变成类型类的实例,所以您可以轻松地对类型进行调度,而无需添加样板值构造函数来包装所有内容。 Haskell 和 Idris 也提供了完全依赖类型的能力。至少,Haskell 是相当主流的,这些类型的东西通常用于非研究目的。
  • 我这样说并不是为了支持 Haskell(我碰巧非常喜欢 Scala)。只有 Scala 使用新的调度行为扩展现有类型的能力需要一定程度的样板文件,这对于从其他地方交给你的大量类型是不可行的(比如在我目前的情况下,使用来自遗留系统,其中类型生成过程无法更改,但它创建的类型必须在以后都进行相同的扩展)。在这种情况下,结构类型的匹配可以解决它。

标签: scala pattern-matching implicit case-class


【解决方案1】:

您的问题存在一些基本的误解。

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

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

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


很难准确说出您要实现的目标,但我怀疑MyEnum1MyEnum2 应该是对象,而不是类。这些继承自您定义的通用接口(@98​​7654325@、findByName)。然后,您可以从公共类型的实例创建一个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)

【讨论】:

  • 枚举类是自动生成的(有数千个,这不能改变,也不能自动生成新的),所以不可能使你描述的特征和枚举扩展它。当我说静态方法时,我的意思是来自伴随对象。在我的情况下,您绝对可以调用MyEnum1.findById,而无需MyEnum1instance。第 3 点在技术上是正确的,但考虑def fn(): Any = {Double},然后fn() 返回object scala.Double。这意味着传递类型本身具有实用意义。说“不能返回类型”太狭隘了。
  • 也许您应该说出您的意思并更准确地使用诸如“类型”和“类”之类的术语?对于我们这些试图帮助您的人来说,这会节省一些精力
  • 我不同意。我花了很多精力来编写问题,以使我所寻求的内容一目了然,并以描述约束的方式(例如,不能扩展新特征,因为这是类所暗示的)自动生成)。我觉得你的前三点是无情的和不合理的迂腐。尽管如此,您编辑的关于结构类型的答案非常有帮助。
【解决方案2】:

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

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. 创建一个伴随对象并使用隐式处理这些情况:

    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)
    

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-11-30
    • 2011-01-24
    • 1970-01-01
    • 1970-01-01
    • 2017-11-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多