matchで型判定がおかしいのはコンパイルのせいで、型タグで解決する

match が意図した挙動にならない

突然ですが下記プログラムの実行結果は何になるでしょうか?

  case class Robot()
  val robo = new Robot
  
  robo match {
    case _: Robot => println("Robot!")
    case l: List[String] => println(s"List String! $l")
    case l: List[Int] => println("List Number!")
    case _ => println("other")
  }

答えはRobot!です。

では次の実行結果は何になるでしょうか?

  List(1,2,3) match {
    case _: Robot => println("Robot!")
    case l: List[String] => println(s"List String! $l")
    case l: List[Int] => println(s"List Number! $l")
    case _ => println("other")
  }

答えはList Number! List(1, 2, 3)ではなく、List String! List(1, 2, 3)!になるのです。

なぜ

下記理由です。

他の JVM言語同様に、Scala の型はコンパイル時に消去 (erase) される。 これは、何らかのインスタンスのランタイム型をインスペクトしてもコンパイル時に Scala コンパイラが持つ型情報を全ては入手できない可能性があることを意味する。 型タグとマニフェスト | Reflection | Scala Documentation

List[T]のTが消えてしまって、一番最初のList[String](型情報が失われているので、List[T]にマッチしている?)にマッチしていると思われます。

対応方法

先述の 型タグとマニフェスト | Reflection | Scala Documentationに書いてあるのですが、型情報を保持してList[Int]にマッチさせるにはTypeTagを使えば良い。

  import scala.reflect.runtime.universe._

  def printListType[T: TypeTag](value:List[T]) :Unit = typeOf[T] match {
    // =:= は型が等しいかを判定
    case l if l =:= typeOf[String] =>  println(s"List String!")
    case l if l =:= typeOf[Int] => println("List Number!")
    case _ => println("other")
  }

  // 型によってprintされる文字が変わった
  printListType(List(1,2,3)) // List Number!
  printListType(List("str")) // List String!

参考

scala: unapplyを覚えたのでリファクタリングしてみる

match caseが読みにくいぞ

やりたいことは、「プレミアムユーザか否か、と年齢によって処理を分ける」です。 case class Userを用いて書かれた下記コードがあるとします。

package advanced.functional

case class User(
                    userId: Long,
                    premium: Boolean,
                    age: Int
                  )


object Test extends App {
  val meg = new User(1L, true, 20)
  val jho = new User(2L, true, 10)

  private def printUserType(user: User) = 
    user match {
      case User(_, true, age) if age >= 18 => println(s"adult premium user")
      case User(_, true, _) => println(s"child premium user")
      case _ =>  println(s"user")
    }

  printUserType(meg)  // adult premium user
  printUserType(jho)  // child premium user
  printUserType(beth) // user

}

match内が可読性が低く、また同じ条件を他のコードで使いまわせないのでリファクタリングしたいですね。

Let's リファクタリング1

case classでは自動でunapply()を適応してくれてるので、ただのclassにしてunapply()を自分で書きなおしてみます。

//case class User(
//                    userId: Long,
//                    premium: Boolean,
//                    age: Int
//                  )

class User(
               val  userId: Long,
               val  premium: Boolean,
               val  age: Int
               )

object User {
  // case classで自動で適応されているunapplyは下記のはず
  def unapply(u: User): Option[(Long, Boolean, Int)] = Some((u.userId,u.premium,u.age))
}

object User のunapplyの中に、match caseで書いていた条件を持っていきましょう。

object User {
  def unapply(u: User): Option[(Boolean, Boolean)]  = Some((u.premium, isAdultUser(u.age)))
  def isAdultUser(age: Int):Boolean = age >= 18
}

object Test extends App {
  val meg = new User(1L, true, 20)
  val jo = new User(2L, true, 10)
  val beth = new User(3L, false, 5)

  private def printUserType(user: User) =
    user match {
      case User(true, true) =>  println(s"adult premium user")
      case User(true, false) =>  println(s"child premium user")
      case _ =>  println(s"user")
    }

  printUserType(meg)  // adult premium user
  printUserType(jho)  // child premium user
  printUserType(beth) // user
}

matchからuserIdの_と、if文が消えてすっきりはしました。が、まだまだベストな気はしません。User(true, true)て何やねんとなります。

Let's リファクタリング2

isAdultUser()というunapplyを作ってはどうでしょう? せっかく書き換えたcase classも元に戻します。

case class User(
                    userId: Long,
                    premium: Boolean,
                    age: Int
                  )

object isAdultUser {
  def unapply(age: Int):Boolean = age >= 18
}

object Test extends App {
  val meg =  User(1L, true, 20)
  val jo =  User(2L, true, 10)
  val beth =  User(3L, false, 5)

  private def printUserType(user: User) =
    (user.premium, user.age) match {
      case (true, isAdultUser()) => println(s"adult premium user")
      case (true, _) => println(s"child premium user")
      case _ =>  println(s"user")
    }

  printUserType(meg)
  printUserType(jo)
  printUserType(beth)
}

おぉ、わかりやすくなった。

      case (true, _) => println(s"child premium user")

は下記ようにはかけないのですね。ここは勉強が足りない。

      case (true,  !isAdultUser()) => println(s"child premium user")
      case (true,  isAdultUser() == false) => println(s"child premium user")

ソフトウェア要求_第一章_ソフトウェア要求の基礎

ソフトウェア要求_第一章_ソフトウェア要求の基礎の読書メモです。

 

ソフトウェア要求ドメインで使用する重要な用語は?

表1-1参照

「プロダクト」要求と「プロジェクト」要求の違いは?

プロダクト要求

構築対象のソフトウェアシステムの特製を記述する要求

プロジェクト要求

プロジェンクト全体をみたときに成功するためには欠かせない期待や成果物(e.g. 開発に必要な物理的リソース、ヘルプデスク用資料)。プロジェクト計画書に入れると良い。

 

要求「開発」と要求「管理」の違いは?

要求開発

要求開発は、引き出し(要求調査)、分析(要求を段階的に精密化)、仕様作成(詳細なものに落とす)、妥当性確認(ユーザーと正確性を確認)の4分野に分割できる。

 

要求管理

常に発生する実際の変更を予期して、プロジェクトに影響を及ぼさないように組み込んでいくことを目的とする。

 

要求で発生する可能性のある問題は?

ユーザーの参加が不十分なために、ビジネス問題を間違えた要求仕様を作ってしまう。結果使用されないソリューションが実装される。