Scala 密封特征和动态case对象

Scala 密封特征和动态case对象,scala,Scala,我有一些枚举实现为密封的traits和case对象。我更喜欢使用ADT方法,因为非详尽的警告,主要是因为我们希望避免类型擦除。像这样的 sealed abstract class Maker(val value: String) extends Product with Serializable { override def toString = value } object Maker { case object ChryslerMaker extends Vend

我有一些枚举实现为密封的traits和case对象。我更喜欢使用ADT方法,因为非详尽的警告,主要是因为我们希望避免类型擦除。像这样的

 sealed abstract class Maker(val value: String) extends Product with Serializable {
    override def toString = value
  }

  object Maker {
    case object ChryslerMaker extends Vendor("Chrysler")
    case object ToyotaMaker extends Vendor("Toyota")
    case object NissanMaker extends Vendor("Nissan")
    case object GMMaker extends Vendor("General Motors")
    case object UnknownMaker extends Vendor("")

    val tipos = List(ChryslerMaker, ToyotaMaker, NissanMaker,GMMaker, UnknownMaker)
    private val fromStringMap: Map[String, Maker] = tipos.map(s => s.toString -> s).toMap

    def apply(key: String): Option[Maker] = fromStringMap.get(key)
  }
到目前为止,这一切都很好,现在我们正在考虑向其他程序员提供访问我们代码的权限,以允许他们在现场进行配置。我看到两个潜在问题: 1) 人们把事情搞得一团糟,写下这样的东西:

case object ChryslerMaker extends Vendor("Nissan")
人们忘了更新tipos

我一直在研究使用一个配置文件(JSON或csv)来提供这些值,并像处理大量其他元素一样读取它们,但我找到的所有答案都依赖于宏,并且似乎极度依赖于使用的scala版本(我们使用2.12)

我想找到的是: 1a)(首选)一种从字符串列表中动态创建案例对象的方法,确保对象的命名与其持有的值一致 1b)(可接受)如果这证明在测试阶段获取对象和值的方法太难 2) 检查列表中元素的数量是否与创建的案例对象的数量匹配

我忘了提到,我已经简单地看了enumeratum,但我不想包括其他库,除非我真的了解其利弊(现在我不确定enumerated与ADT方法相比如何,如果您认为这是最好的方法,并且可以向我指出这样的讨论将非常有效)


谢谢

我想到的一个想法是创建一个 这将读取输入JSON、CSV、XML或任何文件,这是项目的一部分,并将创建一个scala文件

// ----- File: project/VendorsGenerator.scala -----
import sbt.Keys._
import sbt._

/**
 * An SBT task that generates a managed source file with all Scalastyle inspections.
 */
object VendorsGenerator {
  // For demonstration, I will use this plain List[String] to generate the code,
  // you may change the code to read a file instead.
  // Or maybe this will be good enough.
  final val vendors: List[String] =
    List(
      "Chrysler",
      "Toyota",
      ...
      "Unknow"
    )

  val generatorTask = Def.task {
    // Make the 'tipos' List, which contains all vendors.
    val tipos =
      vendors
        .map(vendorName => s"${vendorName}Vendor")
        .mkString("val tipos: List[Vendor] = List(", ",", ")")

    // Make a case object for each vendor.
    val vendorObjects = vendors.map { vendorName =>
      s"""case object ${vendorName}Vendor extends Vendor { override final val value: String = "${vendorName}" }"""
    }

    // Fill the code template.
    val code =
      List(
        List(
          "package vendors",
          "sealed trait Vendor extends Product with Serializable {",
          "def value: String",
          "override final def toString: String = value",
          "}",
          "object Vendors extends (String => Option[Vendor]) {"
        ),
        vendorObjects,
        List(
          tipos,
          "private final val fromStringMap: Map[String, Vendor] = tipos.map(v => v.toString -> v).toMap",
          "override def apply(key: String): Option[Vendor] = fromStringMap.get(key.toLowerCase)",
          "}"
        )
      ).flatten

    // Save the new file to the managed sources dir.
    val vendorsFile = (sourceManaged in Compile).value / "vendors.scala"
    IO.writeLines(vendorsFile, code)
    Seq(vendorsFile)
  }
}
现在,您可以激活源生成器。
此任务将在每次编译步骤之前运行

// ----- File: build.sbt -----
sourceGenerators in Compile += VendorsGenerator.generatorTask.taskValue
请注意,我建议这样做,因为我以前做过,而且我没有任何宏或元编程经验。
另外,请注意,此示例在字符串中传递了大量内容,这使得代码有点难以理解和维护。

顺便说一句,我没有使用枚举数,但快速查看一下它似乎是解决这个问题的最佳方法

编辑
我已经准备好读取HOCON文件并生成匹配的代码。我现在的问题是将scala文件放在项目目录的何处,以及在何处生成文件。我有点困惑,因为似乎有多个步骤:1)编译scala生成器,2)运行生成器,3)编译并构建项目。是这样吗

生成器不是项目代码的一部分,而是元项目的一部分(我知道这听起来很混乱,您可以阅读以了解这一点)-因此,您将生成器放在根级别的
project
文件夹中(用于指定sbt版本的
build.properties
文件位于同一文件夹中)。
如果您的生成器需要一些依赖项(我确信它用于读取HOCON),请将它们放在
项目
文件夹中的
build.sbt
文件中。
如果您计划将单元测试添加到生成器中,您可以在元项目中创建一个完整的scala项目(您可以查看我在其中工作的开源项目(是的,是的,我知道,再次混淆),以供参考)-我个人的建议是,除了测试生成器本身之外,您还应该测试生成的文件,或者最好同时测试两者

生成的文件将自动放置在
src_managed
文件夹中(位于
target
中,因此从源代码版本控制中忽略该文件)。
其中的路径是按顺序排列的,因为编译时默认包括
src_managed
文件夹中的所有内容

val vendorsFile = (sourceManaged in Compile).value / "vendors.scala" // Path to the file to write.`
为了访问源代码中生成的文件中定义的值,只需向生成的文件中添加一个包,并将该包中的值导入代码中(与任何普通文件一样)

您不需要担心与编译顺序相关的任何事情,如果您在
build.sbt
文件中包含源代码生成器,sbt将自动处理所有事情

sourceGenerators in Compile += VendorsGenerator.generatorTask.taskValue // Activate the source generator.
SBT将在每次需要编译生成器时重新运行它

顺便说一句,我在导入中得到“未找到:对象sbt”


如果项目位于元项目空间内,它将在默认情况下找到sbt包,不用担心。

我不明白为什么您不想只使用
枚举
它有一些您直接描述的东西…@Dima,也许他们现在有了,我错过了…这里有一个参考(有很多)@Luis Miguel…我不知道SBT可以做到这一点,我想我应该读一些SBT书籍。是的,我确实感兴趣,我真的很喜欢这种方法。现在我知道了这一点,我甚至可以在代码的其他部分使用。SBT似乎是新的emacs:)不需要引用,我知道如何使用枚举:)如果您有特定的问题或问题,提问…@Dima枚举不提供非穷举模式匹配,并且执行类型擦除,我错了吗?在过去的3年里,我一直在使用ADT,因为这是一种情况,也许我错过了一些东西。这是一个好主意,正是因为你以前做过它(而且它工作正常),我不打算进入scala宏兔子洞!关于enumeratum,我看不到如何以编程方式创建对象,这与我现在的情况相同。我已经准备好了我的代码来读取HOCON文件并生成匹配的代码。我现在的问题是将scala文件放在项目目录的何处,以及在何处生成文件。我有点困惑,因为似乎有多个步骤:1)编译scala生成器,2)运行生成器,3)编译并构建项目。是th吗