Skip to content

Instantly share code, notes, and snippets.

@gakuzzzz

gakuzzzz/text.md Secret

Last active August 29, 2015 14:07
Show Gist options
  • Save gakuzzzz/23786610bd91714d6265 to your computer and use it in GitHub Desktop.
Save gakuzzzz/23786610bd91714d6265 to your computer and use it in GitHub Desktop.
Scala の extractor 文法の拡張

やりたいこと

  • Extractor の unapply/unapplySeq で、タプルやSeqのOptionを返すのではなく、 全ての可能性を示すようなSeqを返せる様にしたい。
    • Empty なら現在の None と同じ動き
    • not-empty なら、先頭から順にパターンマッチを実行していき、 マッチするものが見つかったらそこでマッチの評価を終了する。
  • パターン中に式を書けるようにしたい。
    • 式の結果値を定数パターンとして評価する。
    • 式中では、変数パターンで束縛された変数もスコープに含まれる。

ポーカーのサンプル

参考例として、以下のようなポーカーの役を判定するプログラムを考える。

sealed trait Suite
object Suite {
  case object Spades extends Suite
  case object Hearts extends Suite
  case object Diamonds extends Suite
  case object Clubs extends Suite
  val values: Seq[Suite] = Vector(Spades, Hearts, Diamonds, Clubs)
}
case class Card(number: Int, suite: Suite)

object Main extends App {

  val deck: Seq[Card] = for {
    n <- 1 to 13
    s <- Suite.values
  } yield Card(n, s)

  val hand: Set[Card] = scala.util.Random.shuffle(deck).take(5).toSet

  val rank = Porker.rank(hand)

  println(hand)
  println(rank)

}

現状のScalaのパターンマッチで書くとこうなる

object Porker {

  object Hand {
    def unapply(hand: Set[Card]): Option[(Card, Card, Card, Card, Card)] = {
      implicit val suiteOrdering: Ordering[Suite] = Ordering.by(Suite.values.indexOf)
      implicit val cardOrdering: Ordering[Card] = Ordering.by(unapply)

      // 数字、スートを何らかの順序で一意にソートする必要がある
      PartialFunction.condOpt(hand.toSeq.sorted) {
        case c1 +: c2 +: c3 +: c4 +: c5 +: Nil => (c1, c2, c3, c4, c5)
      }
    }
  }

  def rank(hand: Set[Card]): String = hand match {
    // ソート順を意識してパターンを記述する必要がある
    case Hand(Card(1, s1), Card(10, s2), Card(11, s3), Card(12, s4), Card(13, s5)) if isFlash(s1, s2, s3, s4, s5) => "Loyal Straight Flash"
    case Hand(Card(1, _), Card(10, _), Card(11, _), Card(12, _), Card(13, _)) => "Loyal Straight"
    case Hand(Card(n1, s1), Card(n2, s2), Card(n3, s3), Card(n4, s4), Card(n5, s5)) if isStraight(n1, n2, n3, n4, n5) && isFlash(s1, s2, s3, s4, s5) => "Straight Flash"
    case Hand(Card(n1, _), Card(n2, _), Card(n3, _), Card(n4, _), Card(n5, _)) if (n1 == n2 && n1 == n3 && n1 == n4) || (n2 == n5 && n3 == n5 && n4 == n5) => "Four of Kind"
    case Hand(Card(n1, _), Card(n2, _), Card(n3, _), Card(n4, _), Card(n5, _)) if (n1 == n2 && n1 == n3 && n4 == n5) || (n1 == n2 && n3 == n5 && n4 == n5) => "Full House"
    case Hand(Card(_, s1), Card(_, s2), Card(_, s3), Card(_, s4), Card(_, s5)) if isFlash(s1, s2, s3, s4, s5) => "Flash"
    case Hand(Card(n1, _), Card(n2, _), Card(n3, _), Card(n4, _), Card(n5, _)) if isStraight(n1, n2, n3, n4, n5) => "Straight"
    case Hand(Card(n1, _), Card(n2, _), Card(n3, _), Card(n4, _), Card(n5, _)) if (n1 == n2 && n1 == n3) || (n3 == n5 && n4 == n5) => "Tree of Kind"
    case Hand(Card(n1, _), Card(n2, _), Card(n3, _), Card(n4, _), Card(n5, _)) if (n1 == n2 && n3 == n4) || (n1 == n2 && n4 == n5) || (n2 == n3 && n4 == n5) => "Two Pairs"
    case Hand(Card(n1, _), Card(n2, _), Card(n3, _), Card(n4, _), Card(n5, _)) if (n1 == n2) || (n2 == n3) || (n3 == n4) || (n4 == n5) => "One Pair"
    case _ => "High Card"
  }

  def isStraight(n1: Int, n2: Int, n3: Int, n4: Int, n5: Int): Boolean =
    n1 + 4 == n5 && n2 + 3 == n5 && n3 + 2 == n5 && n4 + 1 == n5

  def isFlash(s1: Suite, s2: Suite, s3: Suite, s4: Suite, s5: Suite): Boolean =
    s1 == s2 && s1 == s3 && s1 == s4 && s1 == s5

}

理想形

最初にあげた2点が実現すると、以下のように書けるようになる。

この例ではパターン中の式を {} で囲むことで表現している。

object Porker {

  object Hand {
    def unapply(hand: Set[Card]): Seq[(Card, Card, Card, Card, Card)] = {
      // このパターンが取りうる可能性を全て列挙して返す
      hand.toSeq.permutations.map {
        case c1 +: c2 +: c3 +: c4 +: c5 +: _ => (c1, c2, c3, c4, c5)
      }.toSeq
    }
  }

  def rank(hand: Set[Card]): String = hand match {
    // 順序を意識せずにパターンの本質だけを記述できる
    case Hand(Card(10, s), Card(11, {s}),    Card(12, {s}),    Card(13, {s}),    Card(1, {s}))     => "Loyal Straight Flash"
    case Hand(Card(10, _), Card(11, _),      Card(12, _),      Card(13, _),      Card(1, _))       => "Loyal Straight"
    case Hand(Card(n, s),  Card({n+1}, {s}), Card({n+2}, {s}), Card({n+3}, {s}), Card({n+4}, {s})) => "Straight Flash"
    case Hand(Card(n, _),  Card({n}, _),     Card({n}, _),     Card({n}, _),     _)                => "Four of Kind"
    case Hand(Card(n1, _), Card({n1}, _),    Card({n1}, _),    Card(n2, _),      Card({n2}, _))    => "Full House"
    case Hand(Card(_, s),  Card(_, {s}),     Card(_, {s}),     Card(_, {s}),     Card(_, {s}))     => "Flash"
    case Hand(Card(n, _),  Card({n+1}, _),   Card({n+2}, _),   Card({n+3}, _),   Card({n+4}, _))   => "Straight"
    case Hand(Card(n, _),  Card({n}, _),     Card({n}, _),     _,                _)                => "Tree of Kind"
    case Hand(Card(n1, _), Card({n1}, _),    Card(n2, _),      Card({n2}, _),    _)                => "Two Pairs"
    case Hand(Card(n, _),  Card({n}, _),     _,                _,                _)                => "One Pair"
    case _                                                                                         => "High Card"
  }

}

更なる例として

ここに更にワイルドカードとして使えるジョーカーを足すとどうなるか。

現状のScalaの文法だと考えたくないが、拡張されたExtractorがあると以下のようにするだけで、rank メソッドは全く変えずにすむ。

sealed trait Card
object Card {
  def unapply(card: Card): Seq[(Int, Suite)] = card match {
    case NumberCard(n, s) => Seq(n -> s)
    case Jorker           => deck.collect { case NumberCard(n, s) => n -> s }
  }
}

case class NumberCard(number: Int, suite: Suite) extends Card
case object Jorker extends Card

val deck: Seq[Card] = Jorker +: (for {
  n <- 1 to 13
  s <- Suite.values
} yield NumberCard(n, s))

問題点

計算効率の問題

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment