【问题标题】:How to map types to values in Scala?如何将类型映射到Scala中的值?
【发布时间】:2021-06-27 01:12:25
【问题描述】:

我认为在 Scala 中Map[Class[_ <: SomeSuperClass], V] 创建了一个不可变映射,使得KclassOf[T],其中TSomeSuperClass 的直接或间接子类。我可能错了,这可能与我的问题相关,也可能不相关。

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

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

给定cards包中的抽象Rank类和子类AceTwoThree、...、Kingblackjack包中的类可以有一个函数如下:

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

【问题讨论】:

  • Scala 3 确实有枚举,如果你愿意尝试的话。
  • @OriginalOriginalOriginalVI 我是。但是我们的行业仍然使用 Java 8?也许不是。

标签: java scala types


【解决方案1】:

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

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

我的建议是,不要抛出,而是明确说明您的函数最终可能会产生错误值。您可以使用Either[MyErrorType, MyValueType] 轻松做到这一点。这样,您最终返回的值必须是 Left(instanceOfMyErrorType)Right(instanceOfMyValueType)。之后,在一些需要处理错误的代码中,您可以随时检查您手中是否有RightLeft,并采取相应措施(例如,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 和一个从RankInt 的部分函数,​​我将返回一个IntError
  • 我现在所做的是,我将提供的函数与将值转换为 Right(thatInteger) 的函数组合在一起(或者,在数学术语中,将后者与前者组合起来;f andThen g 表示 g(f(x))f compose g意思是f(g(x))...我总是觉得使用andThen在精神上更容易)
  • 将上面定义的组合应用于rank 参数,如果该函数没有为rank 定义结果,那么返回Left(error)

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

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

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

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

【讨论】:

  • 嗯。带有案例对象的密封特征。我确实考虑过 case 对象,但我不会想到以这种方式使用它们。谢谢。
  • 没问题。您如何看待偏函数解决方案?
  • 我必须阅读偏函数,我不记得 Cay Horstmann 在他的经典著作中提到过它们。不过,这似乎是一个很简单的概念。
  • 我非常有信心他们在每一本 Scala 书籍中都有,而且它不是一个仅为 Scala 保留的概念。但是你也可以让它与地图一起工作,只要确保使用简单的Map[Rank, Int] 而不是Map[Class[_ &lt;: Rank], Int]。而不是做rank.getClass,你总是可以简单地对排名进行模式匹配,例如rank match { case Ace =&gt; ..., case King =&gt; ... }
  • Map[Rank, Int]?现在我想我可能把整个事情搞得太复杂了。
【解决方案2】:

我认为@slouc 的答案很好(代数数据类型(ADT)/密封特征、错误作为值等),但您确实应该避免使用偏函数(除了某些不会失败的方法,例如 List#collect未定义的输入)并尽可能使用全部功能。你可以将exampleMapping 变成Rank =&gt; Option[Int],但我认为你应该只使用地图。

sealed trait Rank
object Rank {
  //It's up to you if you want to nest these
  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
  case object Nine extends Rank
  //...
}

val exampleMapping = Map(
  Rank.Ace -> 11,
  Rank.King -> 10,
  Rank.Queen -> 10,
  Rank.Jack -> 10,
  Rank.Ten -> 10
)

def valuations(rank: Rank, m: Map[Rank, Int]): Either[Error, Int] =
  m.get(rank).toRight(new Error(s"No such element: $rank"))

如果您实际上不需要变量映射(或者如果映射在编译时已知,那么将它们实现为单独的等级类型也很好)那么您应该只在其中实现值(这不是更像枚举?):

sealed abstract class Rank(val maybeValue: Option[Int])
object Rank {
  case object Ace extends Rank(Some(11))
  case object King extends Rank(Some(10))
  case object Queen extends Rank(Some(10))
  case object Jack extends Rank(Some(10))
  case object Ten extends Rank(Some(10))
  case object Nine extends Rank(None)
  //...
}

def valuations(rank: Rank): Either[Error, Int] =
  rank.maybeValue.toRight(new Error(s"No such element: $rank"))

如果值不是可选的(如 Java 枚举...):

sealed abstract class Rank(val value: Int)
object Rank {
  case object Ace extends Rank(11)
  case object King extends Rank(10)
  case object Queen extends Rank(10)
  case object Jack extends Rank(10)
  case object Ten extends Rank(10)
  case object Nine extends Rank(9)
  //...
}

//we wouldn't even need errors here if they weren't optional, and this method is kind of pointless now...
def valuations(rank: Rank): Int = rank.value

现在有了多个等级类型,您可以通过子类型生成(这会破坏您的 ADT)(等等,我们甚至可以首先对 Java 枚举进行子类化吗?)

def valuations[A <: SuperRank](rank: A): Int = rank.value

但你也可以做类似 RankValue 类型类的东西,其中包含用于不同等级 ADT 的多个实例

//there exist a `RankValue` typeclass instance that maps rank of A to value of B
def valuations[A, B: RankValue[A, *]](rank: A): B = rank.valueOf

但我想我应该在这里停下来,我的目标太多了.​​.....

【讨论】:

    猜你喜欢
    • 2019-09-06
    • 2018-02-28
    • 1970-01-01
    • 2021-10-12
    • 1970-01-01
    • 1970-01-01
    • 2023-03-06
    • 2019-04-09
    • 1970-01-01
    相关资源
    最近更新 更多