2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Scalaの依存型制約を使って、優良顧客リストをクラスレベルの型安全性を担保して生成するコード

Last updated at Posted at 2021-11-08

以下の2つのステップで、課金額を格納する数値リスト__に、「10,000〜10,000,000の範囲内の数値しか格納できない」という制約条件__を、データ型制約として付加してみます。

この記事の続編として、上記の条件に合致する顧客名だけを格納できる、タプル__(ユーザ名, 累積課金金額)__のListを作るコードを書く予定です。

##( 手順 )

【 Step 1 】
数値区間[10000, 10000000]の範囲内にある整数値を、データ型クラス__HighLoyalityCustomerSalesCondition__として定義する。

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

type HighLoyalityCustomerSalesCondition

【 Step 2 】

新しく生成するList型オブジェクトの__型シグネチャを「:List[HighLoyalityCustomerSalesCondition]」__と記述する。

生成されるList型オブジェクトは、__「10,000円〜10,000,000円の範囲内の数値しか格納できない」という制約条件を持つ__List型オブジェクトになる。

そのため、出来上がったListに、別のListを生リテラル値__「List(要素, 要素, ...)」__で束縛(代入)しようとすると、代入を試みる「生リテラル値のList」の全要素が、制約条件を満たす場合のみ、束縛(代入)を許される

  • List(25000, 1000000, 75430)は条件を満たす(全要素が数値範囲にある数字)ので、fineCustomerListに束縛(代入)できる。
sbtConsole
scala> val fineCustomerList: List[HighLoyalityCustomerSalesCondition] =  List(25000, 1000000, 75430)

val fineCustomerList: List[HighLoyalityCustomerSalesCondition] = List(25000, 1000000, 75430)


- List(50, 1, 75)は条件を満たさない(全要素が数値範囲にある数字ではない)ので、fineCustomerListに束縛(代入)できない。errorが返る。
- Errorを返さないように、fineCustomerListを、OptionかEither型を要素に持つListにした方が望ましいかもしれない。

>```Scala:sbtConsole
>scala> val fineCustomerList: List[HighLoyalityCustomerSalesCondition] =  List(50, 1, 75)
>                                                                              ^
>       error: Left predicate of (!(50 < 10000) && (50 < 10000000)) failed: Predicate (50 < 10000) did not fail.
 >                                                                                 ^
 >      error: Left predicate of (!(1 < 10000) && (1 < 10000000)) failed: Predicate (1 < 10000) did not fail.
 >                                                                                   ^
 >      error: Left predicate of (!(75 < 10000) && (75 < 10000000)) failed: Predicate (75 < 10000) did not fail.
>
scala>
>```

####( 別解 )

以下もOK

>```Scala:sbtConsole
>scala> type HighLoyalityCustomerSalesCondition = Int Refined >Interval.ClosedOpen[10000, 10000000]
>type HighLoyalityCustomerSalesCondition
>
>scala> type GoodCustomerList = List[HighLoyalityCustomerSalesCondition]
>type GoodCustomerList
>
>scala> 
>
>scala> val fineCustomerList: GoodCustomerList = List(25000, 1000000, 75430)
>val fineCustomerList: GoodCustomerList = List(25000, 1000000, 75430)
>
>scala> val fineCustomerList: GoodCustomerList = List(50, 1, 75)
>                                                     ^
>       error: Left predicate of (!(50 < 10000) && (50 < 10000000)) failed: Predicate >(50 < 10000) did not fail.
>                                                         ^
>       error: Left predicate of (!(1 < 10000) && (1 < 10000000)) failed: Predicate (1 >< 10000) did not fail.
>                                                            ^
>       error: Left predicate of (!(75 < 10000) && (75 < 10000000)) failed: Predicate 
>(75 < 10000) did not fail.
>
>scala> 
>```


###コード全文

```Scala:sbtConsole
% sbt console

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> 

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

scala> val johnSalesVolume:HighLoyalityCustomerSalesCondition = 500
                                                                ^
       error: Left predicate of (!(500 < 10000) && (500 < 10000000)) failed: Predicate (500 < 10000) did not fail.

scala> val johnSalesVolume:HighLoyalityCustomerSalesCondition = 25000
val johnSalesVolume: HighLoyalityCustomerSalesCondition = 25000


scala> val GoodCustomerSalesVolumeList: List[HighLoyalityCustomerSalesCondition] = List(5000, 1000000, 75430)
                                                                                        ^
       error: Left predicate of (!(5000 < 10000) && (5000 < 10000000)) failed: Predicate (5000 < 10000) did not fail.

scala> val GoodCustomerSalesVolumeList: List[HighLoyalityCustomerSalesCondition] = List(5000, 100, 75)
                                                                                        ^
       error: Left predicate of (!(5000 < 10000) && (5000 < 10000000)) failed: Predicate (5000 < 10000) did not fail.
                                                                                              ^
       error: Left predicate of (!(100 < 10000) && (100 < 10000000)) failed: Predicate (100 < 10000) did not fail.
                                                                                                   ^
       error: Left predicate of (!(75 < 10000) && (75 < 10000000)) failed: Predicate (75 < 10000) did not fail.

scala> val GoodCustomerSalesVolumeList: List[HighLoyalityCustomerSalesCondition] = List(500000, 100000, 750)
                                                                                                        ^
       error: Left predicate of (!(750 < 10000) && (750 < 10000000)) failed: Predicate (750 < 10000) did not fail.


scala> val fineCustomerList: List[HighLoyalityCustomerSalesCondition] =  List(25000, 1000000, 75430)
val fineCustomerList: List[HighLoyalityCustomerSalesCondition] = List(25000, 1000000, 75430)

scala> val fineCustomerList: List[HighLoyalityCustomerSalesCondition] =  List(50, 1, 75)
                                                                              ^
       error: Left predicate of (!(50 < 10000) && (50 < 10000000)) failed: Predicate (50 < 10000) did not fail.
                                                                                  ^
       error: Left predicate of (!(1 < 10000) && (1 < 10000000)) failed: Predicate (1 < 10000) did not fail.
                                                                                     ^
       error: Left predicate of (!(75 < 10000) && (75 < 10000000)) failed: Predicate (75 < 10000) did not fail.

scala> val fineCustomerList: List[HighLoyalityCustomerSalesCondition] =  List(500000, 1, 75)
                                                                                      ^
       error: Left predicate of (!(1 < 10000) && (1 < 10000000)) failed: Predicate (1 < 10000) did not fail.
                                                                                         ^
       error: Left predicate of (!(75 < 10000) && (75 < 10000000)) failed: Predicate (75 < 10000) did not fail.

scala> val fineCustomerList: List[HighLoyalityCustomerSalesCondition] =  List(500000, 1000000, 75)
                                                                                               ^
       error: Left predicate of (!(75 < 10000) && (75 < 10000000)) failed: Predicate (75 < 10000) did not fail.

scala> val fineCustomerList: List[HighLoyalityCustomerSalesCondition] =  List(50, 1000000, 75000)
                                                                              ^
       error: Left predicate of (!(50 < 10000) && (50 < 10000000)) failed: Predicate (50 < 10000) did not fail.

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

scala> type GoodCustomerList = List[HighLoyalityCustomerSalesCondition]
type GoodCustomerList

scala> 

scala> val fineCustomerList: GoodCustomerList = List(25000, 1000000, 75430)
val fineCustomerList: GoodCustomerList = List(25000, 1000000, 75430)

scala> val fineCustomerList: GoodCustomerList = List(50, 1, 75)
                                                     ^
       error: Left predicate of (!(50 < 10000) && (50 < 10000000)) failed: Predicate (50 < 10000) did not fail.
                                                         ^
       error: Left predicate of (!(1 < 10000) && (1 < 10000000)) failed: Predicate (1 < 10000) did not fail.
                                                            ^
       error: Left predicate of (!(75 < 10000) && (75 < 10000000)) failed: Predicate (75 < 10000) did not fail.

scala> 

###RefineVを使った場合

RefineVのForall述語を使うと、1行書きできる。

  • Forallで、Listに格納されたすべての要素が、指定した述語制約を充足するか否かを検査する
sbtConsole
scala> val v1 = List(25000, 1000000, 75430)
val v1: List[Int] = List(25000, 1000000, 75430)

scala> refineV[Forall[Equal[-10]]](v1)
val res12: Either[String,eu.timepit.refined.api.Refined[List[Int],eu.timepit.refined.collection.Forall[eu.timepit.refined.generic.Equal[-10]]]] = Left(Predicate failed: ((25000 == -10) && (1000000 == -10) && (75430 == -10)).)

scala> refineV[Forall[Interval.ClosedOpen[10000, 10000000]]](v1)
val res13: Either[String,eu.timepit.refined.api.Refined[List[Int],eu.timepit.refined.collection.Forall[eu.timepit.refined.numeric.Interval.ClosedOpen[10000,10000000]]]] = Right(List(25000, 1000000, 75430))

scala> refineV[Forall[Interval.ClosedOpen[10000, 10000000]]](List(25000, 1000000, 75430))
val res14: Either[String,eu.timepit.refined.api.Refined[List[Int],eu.timepit.refined.collection.Forall[eu.timepit.refined.numeric.Interval.ClosedOpen[10000,10000000]]]] = Right(List(25000, 1000000, 75430))

scala>

ただし、refineVで述語定義した型に、名前をつける方法が見当たらない。

そのため、独自定義のデータ型クラスとして命名して、定義したクラス名を、他のデータや関数の型シグネチャの要素として組み込むことができない。

sbtConsole
scala> type NumListElementRange = List[Int] refineV[Forall[Interval.ClosedOpen[10000, 10000000]]](List(25000, 1000000, 75430))
                                                   ^
       error: identifier expected but '[' found.

scala> 
sbtConsole
scala> val fineCustomerList: refineV[Forall[Interval.ClosedOpen[10000, 10000000]]] =  List(25000, 1000000, 75430)
                             ^
       error: not found: type refineV

scala> 

###( 参考 )

数値範囲条件 [10000, 10000000]に該当する要素だけをList[Int]の要素に束縛する処理は、普通にfor...yield文を実行することでも実現できる。

( 参考 )

しかし、データ型レベルの(型)制約条件をListに付加させておく方が、後々、for...yield文で代入する値の検査を忘れるリスクを取り除くことができるため、より安全である。

sbtConsole
scala> val v1 = List(25000, 1000000, 75430)
val v1: List[Int] = List(25000, 1000000, 75430)

scala> val trailList = for (elem <- v1 if (elem >= 10000 && elem <= 10000000)) yield elem
val trailList: List[Int] = List(25000, 1000000, 75430)

scala> trailList
val res1: List[Int] = List(25000, 1000000, 75430)

scala> val v2 = List(10, 56700, 300)
val v2: List[Int] = List(10, 56700, 300)

scala> val trailList2 = for (elem <- v2 if (elem >= 10000 && elem <= 10000000)) yield elem
val trailList2: List[Int] = List(56700)

scala> trailList2
val res3: List[Int] = List(56700)

scala> 

###リテラル値でない、すでに定義済みのオブジェクトは、要素に含められない

sbtConsole
scala> val sales_volume = 200000
val sales_volume: Int = 200000

scala> sales_volume
val res16: Int = 200000

scala> val fineCustomerList3: List[HighLoyalityCustomerSalesCondition] =  List(sales_volume)
                                                                               ^
       error: compile-time refinement only works with literals

scala> List(sales_volume)
val res17: List[Int] = List(200000)

scala> 

この問題は、以下の記事で解決しました。

##( 備考 )

###build.sbtファイル

build.sbt
scalaVersion := "2.13.6"
libraryDependencies += "eu.timepit" %% "refined" % "0.9.27"

上記のbuild.sbtファイルを読み込んで、refinedライブラリをインストールする必要があるので、scalaコマンドではなく、sbt consoleコマンドでREPLを起動する。

Terminal
% sbt console
WARNING: A terminally deprecated method in java.lang.System has been called
WARNING: System::setSecurityManager has been called by sbt.TrapExit$ (file:/Users/electron/.sbt/boot/scala-2.12.14/org.scala-sbt/sbt/1.5.5/run_2.12-1.5.5.jar)
WARNING: Please consider reporting this to the maintainers of sbt.TrapExit$
WARNING: System::setSecurityManager will be removed in a future release
[info] welcome to sbt 1.5.5 (Homebrew Java 17.0.1)
[info] loading project definition from /Users/electron/scala_trial/refined_trial/project
[info] loading settings for project refined_trial from build.sbt ...
[info] set current project to refined_trial (in build file:/Users/electron/scala_trial/refined_trial/)
[info] Starting scala interpreter...
Welcome to Scala 2.13.6 (OpenJDK 64-Bit Server VM, Java 17.0.1).
Type in expressions for evaluation. Or try :help.

scala> import eu.timepit.refined._

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

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

scala> import eu.timepit.refined.api.{RefType, Refined}
import eu.timepit.refined.api.{RefType, Refined}

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

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

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

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

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

scala> import shapeless.{ ::, HNil }
import shapeless.{$colon$colon, HNil}

scala>
2
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
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?