首页 > 解决方案 > 如何将类型映射到Scala中的值?

问题描述

我认为在 Scala 中创建Map[Class[_ <: SomeSuperClass], V]了一个不可变映射,这K就是. 我可能是错的,这可能与我的问题有关,也可能无关。classOf[T]TSomeSuperClass

我发现我无法真正访问这些V值。如何将 的子类映射SomeSuperClassV值?

例如,打牌。在二十一点中,J、Q 和 K 的值都与 10 相同。但在其他游戏中,它们可能具有不同的值,例如 J 的值是 11,Q 的值是 12,K 的值是 13。

给定包中的抽象Rank类和子类Ace, Two, Three, ..., King,cards包中的类blackjack可以具有如下函数:

def valueOf(rank: Rank): Int = rank match {
  case _: Ace => 11 // TODO: Figure out when value 1
  case _: King => 10
  case _: Queen => 10
  case _: Jack => 10
  case _: Ten => 10
  case _: Nine => 9
  case _: Eight => 8
  // etc.
}

在 Java 中,我会(并且确实)只使用枚举类型。Scala 中有一种叫做密封类的东西,但实际上这似乎比 Java 中的枚举要麻烦得多。或者也许我还没有弄清楚如何正确使用它们。

但我更喜欢一种更灵活的方法,它允许我或其他任何人添加卡片等级,并以最少的麻烦(例如,通过简单的子类化Rank)对这些等级进行估值。像这样的东西:

class Valuations(val map: Map[Class[_ <: Rank], Int]) {

  def valueOf(rank: Rank): Int = {
    val key = rank.getClass
    if (map.contains(key) map.get(key) else {
      throw new NoSuchElementException("No match for " + rank.toString)
    }
  }

}

我可以让 IntelliJ 给我一个绿色复选标记,但我无法让我的单元测试通过。

在 Java 版本中,枚举类型Rank具有值函数,因此您可以依赖Rank实例来获得该值。但是,即使我不想添加新的卡片等级,向其他类型的值添加新的映射int也可能很笨拙,并且不符合单一责任原则的精神。

Scala 中肯定有更优雅的解决方案。如何将这些类型匹配为值的键,同时保持添加不同子类型的灵活性,而无需重写任何已经存在的内容?

标签: javascalatypes

解决方案


你真正想要的是从Rankto的偏函数Int。与为域的所有元素定义的总函数不同,部分函数只能为其中的一些元素定义。

此外,如果您不介意另一个建议,抛出异常不符合函数式编程的精神。我没有强加函数式方法,因为 Scala 非常适合以面向对象(类似 Java)的方式使用。但我会建议它。它说 - 不要扔!我总是说投掷就像完全撕裂你的程序结构。它撕开墙壁说“哎呀,现在我要走捷径了”。这几乎就像 goto 语句。如果您决定使用异常,我很确定您可以修改我的示例以合并它们。

我的建议是,不要抛出,而是明确表明您的函数最终可能会产生错误值。您可以使用Either[MyErrorType, MyValueType]. 这样,您最终返回的值必须是Left(instanceOfMyErrorType)or Right(instanceOfMyValueType)。之后,在一些需要处理错误的代码中,您可以随时检查您手中是否有 aRight或 a Left,并采取相应的行动(例如,在 Left 记录错误并返回 400 HTTP 响应或其他情况下)。

这是代码:

sealed trait Rank
case object Ace extends Rank
case object King extends Rank
case object Queen extends Rank
case object Jack extends Rank
case object Ten extends Rank
// ...

final case class Error(msg: String)

val exampleMapping: PartialFunction[Rank, Int] = 
  (rank: Rank) => rank match {
    case Ace => 11 
    case King => 10
    case Queen => 10
    case Jack => 10
    // No Ten!
  }

def valuations(rank: Rank, f: PartialFunction[Rank, Int]): Either[Error, Int] =
  f.andThen(rank => Right(rank)).applyOrElse(
    rank,
    (v: Rank) => Left(Error(s"No such element: $v"))
  )

val a = valuations(King, exampleMapping) // Right(10)
val b = valuations(Ten, exampleMapping)  // Left(Error(No such element: Ten))

上面代码中最有趣的部分是valuations方法。这是它的作用:

  • 给我一个rank: Rank和一个偏函数 from Rankto Int,我将返回一个Int或一个Error
  • 我现在所做的是,我将提供的函数与将值转换为的函数组合在一起Right(thatInteger)(或者,用数学术语,将后者与前者组合;f andThen g意味着g(f(x))f compose g意味着f(g(x))......我总是觉得它在精神上更容易使用andThen
  • 将上面定义的组合应用于rank参数,如果该函数没有为 定义的结果rank,那么返回Left(error)

当然,你可以有很多不同的部分函数,​​其他用户可以定义他们自己的,等等。但请注意两点:

  • 使用密封特性意味着没有人可以扩展Rank。这对我来说很有意义。但是,如果您不仅希望以后能够添加不同的映射,甚至还希望添加新Rank的 s,那么就不要让 trait 密封。
  • 将您的案例类设为最终类,因为它们不应该被扩展,并且通过使它们成为最终类,如果您尝试定义一个完整的函数(不是部分的!),编译器会警告您,但忘记处理某些情况,因为意外省略了case某个等级。对于 case 对象,也可以将它们标记为 final,但这不是强制性的,因为它们无论如何都不能扩展(它们只能有一个实例,如 Java 中的单例)。

如果出于某种原因您稍后决定不想在未定义映射时返回错误,而是想使用默认映射,该映射具有所有等级的所有默认值(= 它不是偏函数Rank => Int,而是总共一个),那么这也很容易做到。试试看练习!

最后一点,Scala 3 将有非常好的枚举,这是你说你从 Java 中熟悉的东西,而 Scala 2 缺乏。


推荐阅读