1
0

More than 1 year has passed since last update.

【Scala】値制約(Refined依存型)をmatchのcase条件節で使う方法:case classを使え!

Last updated at Posted at 2021-11-10

以前書いた記事では、ユーザ定義の依存型のクラス名(type別名)を使って、型レベルのパターンマッチを試みに成功しませんでした。

Refinedで定義した値制約に、typeキーワード別名ConditionedIntを付けた後、

sbtConsole
scala> type ConditionedInt = Int Refined Interval.ClosedOpen[10000, 10000000]
type ConditionedInt

scala>

case節のマッチング対象に、Stringと並んで、ConditionedIntを指定すると、認識されない。

sbtConsole
scala> def class_check(a: Any) = {
    |   a match {
   省略
    |     case _: ConditionedInt => "有料顧客の課金額"

( エラー表示 )

sbtConsole
           case _: ConditionedInt => "有料顧客の課金額"
                   ^
On line 4: warning: non-variable type argument Int in type pattern eu.timepit.refined.api.Refined[Int,eu.timepit.refined.numeric.Interval.ClosedOpen[10000,10000000]] (the underlying of ConditionedInt) is unchecked since it is eliminated by erasure

今回、以下の方法で成功することが分かりましたので、記事にしてみました。

  1. Refinedを使って、値制約をユーザ定義する。定義した値条件にtypeキーワードで別名をつける。
  2. 上記の別名型を持つオブジェクトを受け取るコンストラクタが定義されたcase classを、ユーザ定義クラスとして用意。
  3. matchのcase節に、IntやStringやList[String], List[Int]と並べて、上記のcase classのクラス名をセットする。

Step 1

Refinedを使って、値制約をユーザ定義する。定義した値条件にtypeキーワードで別名をつける。

sbtConsole
scala> type ConditionedInt = Int Refined Interval.ClosedOpen[10000, 10000000]
type ConditionedInt

scala>

Step 2

上記の別名型を持つオブジェクトを受け取るコンストラクタが定義されたcase classを、ユーザ定義クラスとして用意する。

sbtConsole
scala> case class DefinedInt(number: ConditionedInt) {
     |   val numeric = number
     |   def getNumber = numeric
     |   println(numeric)
     | }
class DefinedInt

scala> 
  • 挙動を確認。
  • 確かに、定義した値制約条件がちゃんと動いている。
sbtConsole
scala> val new_number = new DefinedInt(2000000)
2000000
val new_number: DefinedInt = DefinedInt(2000000)

scala> val new_number = new DefinedInt(200)
                                       ^
       error: Left predicate of (!(200 < 10000) && (200 < 10000000)) failed: Predicate (200 < 10000) did not fail.

scala> 
  • インスタンスメソッドも動く
sbtConsole
scala> new_number.getNumber
val res9: ConditionedInt = 2000000

Step 3

  • match文のcase節に、IntやStringやList[String], List[Int]と並べて、上記のcase classのクラス名をセットする。
sbtConsole
scala> def class_check(a: Any) = {
     |  a match {
     |    case _: String => "文字列"
     |    case _: DefinedInt => "値が指定範囲内の数値"
     |    case _ => "不明"
     |  }
     | }
def class_check(a: Any): String

scala> 

Step 4

  • 値制約を充足する数値(例:2000000)を初期化変数として、DefinedIntクラスのコンストラクタに渡して、DefinedInt型オブジェクトのインスタンスを生成する。
  • 生成されたインスタンス・オブジェクトを、match式に渡す。
  • 意図した通り、値が指定範囲内の数値というメッセージ(String型)が出力された。
sbtConsole
scala> val new_number = new DefinedInt(2000000)
2000000
val new_number: DefinedInt = DefinedInt(2000000)

scala>
scala> class_check(new_number)
val res10: String = 値が指定範囲内の数値

scala>

( 挙動の補足 )

  • 数値リテラルで、数値範囲内にある数値を渡すと、文字列(String)として認識される。
  • newキーワードで生成したDefinedInt型クラスのインスタンスオブジェクトを渡さないと、DefinedInt型のcase節には合致しない。
sbtConsole
scala> class_check(1000000)
val res11: String = 不明

scala> 

以下もOK

sbtConsole
scala> def class_check2(a: Any) = {
     |  a match {
     |    case _: String => "文字列"
     |    case _: DefinedInt => "値が指定範囲内の数値"
     |    case _: Int => "普通の整数"
     |    case _ => "不明"
     |  }
     | }
def class_check2(a: Any): String

scala> 

scala> class_check2(new_number)
val res12: String = 値が指定範囲内の数値

scala> class_check2(1000000)
val res13: String = 普通の整数

scala> class_check2(10000000)
val res14: String = 普通の整数

scala> class_check2(100)
val res15: String = 普通の整数

scala> class_check2("abc")
val res16: String = 文字列

scala> class_check2('a')
val res17: String = 不明

scala> class_check2(List(1, 2, 3))
val res18: String = 不明

scala> 

普通のユーザ定義クラスは、match文の中のcase節で認識されない

sbtConsole
scala> type ConditionedInt = Int Refined Interval.ClosedOpen[10000, 10000000]
type ConditionedInt


scala> class numberConditionedInt(val number: ConditionedInt) {
     |    val num = number
     |    def get_number: ConditionedInt =  num
     | }
class numberConditionedInt

scala> 
sbtConsole
scala> val newConditionedNumber = new numberConditionedInt(200000)
val newConditionedNumber: numberConditionedInt = numberConditionedInt@66354fc2

scala> println(newConditionedNumber)
$line15.$read$$iw$numberConditionedInt@66354fc2

scala> newConditionedNumber
val res2: numberConditionedInt = numberConditionedInt@66354fc2

scala> newConditionedNumber.get_number
val res3: ConditionedInt = 200000

scala> println(newConditionedNumber.get_number)
200000

scala> val newConditionedNumber = new numberConditionedInt(200)
                                                           ^
       error: Left predicate of (!(200 < 10000) && (200 < 10000000)) failed: Predicate (200 < 10000) did not fail.

scala> 
sbtConsole
scala> def class_check(a: Any) = {
     |  a match {
     |    case _: String => "文字列"
     |    case _: numberConditionedInt => "値が指定範囲内の数値"
     |    case _: Int => "普通の整数"
     |    case _ => "不明"
     |  }
     | }
def class_check(a: Any): String

scala> 
  • numberConditionedInt型の値制約を充足する数値1000000)を与えても、numberConditionedInt型のcase節として認識されない。
sbtConsole
scala> class_check(1000000)
val res6: String = 普通の整数

scala>
  • 以下も同様にダメ。
sbtConsole
scala> def class_check2(a: Any) = {
     |  a match {
     |    case _: String => "文字列"
     |    case _: numberConditionedInt => "値が指定範囲内の数値"
     |    case _ => "不明"
     |  }
     | }
def class_check2(a: Any): String

scala> 

scala> class_check2(1000000)
val res7: String = 不明

scala> 

実行環境

インポートした外部ライブラリ

sbtConsole
scala> import eu.timepit.refined._
import eu.timepit.refined._

scala> import eu.timepit.refined.api.Refined
import eu.timepit.refined.api.Refined

scala> import eu.timepit.refined.auto._
import eu.timepit.refined.auto._

scala> import eu.timepit.refined.numeric._
import eu.timepit.refined.numeric._

scala> 

Refinedのバージョン:2.13.6

  • build.sbtファイル_の記述内容
build.sbt
scalaVersion := "2.13.6"
libraryDependencies += "eu.timepit" %% "refined" % "0.9.27"
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0