如何将类型映射到Scala中的值?

我认为,在Scala中Map[Class[_ <: SomeSuperClass], V]创建一个不可变的映射,从而KclassOf[T]地方T是直接或间接的子类SomeSuperClass。我在这一点上可能是错的,这可能与我的问题有关,也可能无关。

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

例如,打牌。在二十一点中,千斤顶、皇后和国王的价值都与十相同。但是在其他游戏中,它们可能具有不同的值,例如 jack 的值是 11,皇后的值是 12,而国王的值是 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 中有一个更优雅的解决方案。如何将这些类型作为值的键进行匹配,同时保持添加不同子类型的灵活性,而无需重写任何已存在的内容?

回答

您真正想要的是从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和一个来自Rankto的部分函数Int,我将返回一个Int或一个Error
  • 我现在所做的是,我将提供的函数与将值转化为的函数组合在一起Right(thatInteger)(或者,用数学术语来说,将后者与前者组合;f andThen g意味着g(f(x))f compose g意味着f(g(x))......我总是发现它在精神上更容易使用andThen
  • 将上面定义的组合应用于rank参数,如果该函数没有定义的结果rank,那么返回Left(error)

当然,您可以拥有许多不同的此类部分功能,其他用户可以定义自己的功能等。但请注意以下两点:

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

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

最后要注意的是,Scala 3 将有非常好的枚举,这是您说在 Java 中很熟悉的东西,而 Scala 2 则没有。

  • I'm pretty confident they're in every Scala book, plus it's not a concept reserved for Scala only. But you can make it work with a Map too, just make sure to use a simple `Map[Rank, Int]` instead of `Map[Class[_ <: Rank], Int]`. And instead of doing `rank.getClass`, you can always simply pattern match on the rank, e.g. `rank match { case Ace => ..., case King => ... }`

以上是如何将类型映射到Scala中的值?的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>