簡體   English   中英

如何將 map 類型轉換為 Scala 中的值?

[英]How to map types to values in Scala?

我認為在 Scala Map[Class[_ <: SomeSuperClass], V]創建一個不可變的 map 使得KclassOf[T]其中TSomeSuperClass的直接或間接子類。 我可能是錯的,這可能與我的問題有關,也可能無關。

我發現我無法真正訪問V值。 如何將 SomeSuperClass 的子類SomeSuperClass轉換為V值?

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

Given an abstract Rank classes and subclasses Ace , Two , Three , ..., King , in the cards package, a class in the blackjack package could have a function like this:

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具有值 function,因此您可以依賴Rank實例來獲得該值。 但是,即使我不想添加新的卡片等級,將新的映射添加到除int以外的類型的值也可能很笨拙,並且不符合單一責任原則的精神。

Scala 肯定有更優雅的解決方案。 如何將這些類型匹配為值的鍵,同時保持添加不同子類型的靈活性,而無需重寫任何已經存在的內容?

您真正想要的是從RankInt的部分 function 。 與為域的所有元素定義的總函數不同,部分函數只能為其中的一些元素定義。

此外,如果您不介意另一個建議,拋出異常不符合函數式編程的精神。 我沒有對您強加功能方法,因為 Scala 非常適合以面向 object 的方式(類似 Java)使用。 但我會建議它。 它說 - 不要扔。 我總是說,投擲就像完全撕裂你的程序的結構,它會撕裂牆壁並說“哎呀。現在我正在走捷徑”。 這幾乎就像 goto 語句,如果你決定使用異常。 我很確定您可以修改我的示例以合並它們。

我的建議是,不要拋出,而是明確說明您的 function 最終可能會導致錯誤值。 您可以使用Either[MyErrorType, MyValueType]輕松做到這一點。 這樣,您最終返回的值需要是Left(instanceOfMyErrorType)Right(instanceOfMyValueType) 之后,在一些需要處理錯誤的代碼中,您始終可以檢查您手中是Right還是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和從RankInt的部分 function 我將返回一個Int或一個Error
  • 我現在要做的是,我將提供的 function 與將該值轉換為Right(thatInteger)的那個組合在一起(或者,在數學術語中,將后者與前者組合起來; f andThen g表示g(f(x))f compose g意味着f(g(x)) ...我總是覺得它在精神上更容易使用andThen )
  • 將上面定義的組合應用於rank參數,如果 function 沒有定義rank的結果,那么返回Left(error)

當然,你可以有很多不同的部分函數,其他用戶可以定義他們自己的,等等。但請注意兩點:

  • 使用封印特性意味着沒有人可以擴展Rank 這對我來說很有意義。 但是,如果您希望以后不僅可以添加不同的映射,甚至還可以添加新的Rank ,那么就不要將 trait 密封。
  • 使您的案例類成為最終的,因為它們不應該被擴展,並且通過使它們成為最終的,如果您嘗試定義一個總的 function(不是部分的),編譯器會警告您,但忘記處理某些案例,因為不小心忽略了某個等級的case . 對於 case 對象,也可以將它們標記為 final,但這不是強制性的,因為它們無論如何都不能擴展(它們只能有一個實例,如 Java 中的 singleton)。

如果出於某種原因您稍后決定不想在未定義映射時返回錯誤,而是想使用默認映射,該映射具有所有等級的所有默認值(= 它不是部分 function Rank => Int ,但總共是一個),那么這也很容易做到。 試試看練習!

最后一點,Scala 3 將有非常好的枚舉,這是你說你從 Java 熟悉的東西,而 Scala 2 缺乏。

我認為@slouc 的答案很好(代數數據類型(ADTs)/密封特征,錯誤作為值等),但你真的應該避免部分 function (除了某些方法,如List#collect在未定義的輸入上不會失敗)並盡可能使用全部功能。 您可以將exampleMapping設置為Rank => Option[Int]但我認為您應該只使用 map 代替。

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

但我想我應該在這里停下來,我要走的太多了……

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM