Scala 3におけるinlineを用いたコンパイル時の様々なパターンマッチングと条件分岐
はじめに
Scala 3では言語設計が一新され多くの新機能が追加されました。その一環として、Scala 3ではメタプログラミングの全面的な見直しが行われました。
見直しが行われた機能の中にはScala 2から存在していたinline
も含まれていました。
今回はScala 3から大きく機能が拡張されたinline
修飾子に関しての話であり、特にinline
を使用したパターンマッチングと条件分岐の使用方法に関しての話となります。
まずScala3のinline
とinline
と組み合わせて使用できるAPIの紹介を行い、その後にそれぞれを組み合わせて行うパターンマッチングと条件分岐の使用方法に関して説明できればと思います。
Scala3のinline
Scala 3で見直されたinline
修飾子は、Scala 2の時から@inline
アノテーションとして存在していました。しかし、Scala 2でのinline
修飾子は、単なるヒントでありコードがインライン化されることを保証することはできませんでした。
Scala 3で見直されたことによって、inline
修飾子はコードがインライン化されていることを保証し、インライン化できない場合はコンパイルエラーを出すことができるようになりました。
インライン化の簡単な例として以下のようなコードがあるとします。
inline val debugLogEnabled = true
@main def hello: Unit =
if debugLogEnabled then
println("Hello world!")
inline val debugLogEnabled = true
はインライン定数であり、debugLogEnabled
への参照は全てコンパイル時に評価された値となります。
つまりdebugLogEnabled
という名前の変数はコンパイル時に評価が行われて、右辺のtrueに置き換えられるようになります。
上記コードをコンパイルするとdebugLogEnabled
は右辺値であるtrueに置き換えられるので、コンパイル時には以下のようになります。
@main def hello: Unit =
if true then // debugLogEnabledは変数への参照ではなく定数である値に置き換えられる
println("Hello world!")
最終的にはコンパイラによって、true値は定数であるため、if条件は単純化されて以下のようになります。
@main def hello: Unit =
println("Hello world!")
inline
修飾子を使用したインライン化は、このようにコンパイル時に評価を行うことによってコードを最適化します。
compiletimeパッケージ
Scala 3では新しいパッケージscala.compiletimeが導入され、インラインコードを評価してコンパイル時にユーザー独自のエラー生成や条件判定ができるようになりました。
constValue/constValueOpt
constValue
は型によって表される定数値を生成する関数であり、型が定数型でない場合はコンパイル時にエラーとなります。
つまり型パラメーターから実際に使用できる値を生成できるということです。
以下に定義されているshowTypeParamInt
は型パラメーターでInt型を受け取りIntを返す関数です。
scala.compiletime.constValue[N]
を見ると受け取った型パラメーターをconstValue
に渡しそのまま戻り値として返しています。
inline def showTypeParamInt[N <: Int]: Int = scala.compiletime.constValue[N]
showTypeParamInt
を呼び出すと以下のようになります。
scala> showTypeParamInt[1]
val res1: Int = 1
scala> showTypeParamInt[2]
val res2: Int = 2
scala> showTypeParamInt[4]
val res3: Int = 4
constValue
は定数値しか受け取ることができないため定数値ではない値を渡すとコンパイルエラーとなります。
scala> val int: Int = 1
val int: Int = 1
scala> showInt[int.type]
-- Error: ----------------------------------------------------------------------
1 |showInt[int.type]
|^^^^^^^^^^^^^^^^^
|not a constant type: (int : Int); cannot take constValue
|-----------------------------------------------------------------------------
|Inline stack trace
|- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|This location contains code that was inlined from rs$line$7:1
1 |inline def showInt[N <: Int]: Int = scala.compiletime.constValue[N]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-----------------------------------------------------------------------------
1 error found
constValueOpt
はconstValue
と同じように型によって表される定数値を生成する関数ですが、constValue
と違い型が定数型でない場合はコンパイル時にエラーとはならずOption型であるNoneを返します。
先ほどのshowTypeParamInt
をconstValueOpt
に変更してみると戻り値がOption型になっていることがわかります。
inline def showTypeParamInt[N <: Int]: Option[Int] = scala.compiletime.constValueOpt[N]
再度showTypeParamInt
を呼び出すと以下のようになります。
scala> showTypeParamInt[4]
val res4: Option[Int] = Some(4)
先ほどと同じように定数値ではない値を渡した場合はNoneが返却されます。
scala> showTypeParamInt[int.type]
val res5: Option[Int] = None
constValue/constValueOpt
を使用することで型のみでプログラミングを行えるようになりました。
constValue
を使用することで定数値である型パラメーターを値として扱えるようになったので、値をそのまま返すだけではなくパターンマッチングを型だけで行えるようにもなりました。
以下はconstValue
を使用して月にマッチする数字が型パラメーターに渡された時にその英名を返すようなメソッドです。
inline def renameMonth[N <: Int]: String =
inline scala.compiletime.constValue[N] match
case 1 => "January"
case 2 => "February"
case 3 => "March"
case 4 => "April"
case 5 => "May"
case 6 => "June"
case 7 => "July"
case 8 => "August"
case 9 => "September"
case 10 => "October"
case 11 => "November"
case 12 => "December"
対応する定数値を渡すとそれぞれの月にマッチした英名が返却されます。
scala> renameMonth[1]
val res1: String = January
scala> renameMonth[12]
val res2: String = December
定数値を渡してもパターンマッチに存在しない値であった場合コンパイルエラーとなります。
scala> renameMonth[13]
-- Error: ----------------------------------------------------------------------
1 |renameMonth[13]
|^^^^^^^^^^^^^^^
|cannot reduce inline match with
| scrutinee: 13 : (13 : Int)
| patterns : case 1
| case 2
| case 3
| case 4
| case 5
| case 6
| case 7
| case 8
| case 9
| case 10
| case 11
| case 12
|----------------------------------------------------------------------------
|Inline stack trace
|- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|This location contains code that was inlined from rs$line$3:2
2 | inline scala.compiletime.constValue[N] match
| ^
3 | case 1 => "January"
4 | case 2 => "February"
5 | case 3 => "March"
6 | case 4 => "April"
7 | case 5 => "May"
8 | case 6 => "June"
9 | case 7 => "July"
10 | case 8 => "August"
11 | case 9 => "September"
12 | case 10 => "October"
13 | case 11 => "November"
14 | case 12 => "December"
----------------------------------------------------------------------------
1 error found
erasedValue
erasedValue
は先ほどのconstValue
とは違い渡された型をあたかも値として存在するかのように振る舞うことができる関数です。
どういうことかというとScalaでは渡された型でパターンマッチングなどの条件分岐を行いたい場合実際に値を使用しなければいけませんでした。
しかしerasedValue
を使うことで、値を使用することなく型のみで条件分岐を行い、条件分岐に応じた処理の結果を返すことができるようになります。
例えば受け取った型の情報がOptionかどうかを判定する場合にerasedValue
が存在する場合としない場合でコードを見比べてみます。
erasedValue
が存在しない場合
def isOptional[T](value: T): Boolean =
value match
case _: Option[?] => true
case _ => false
型がOptionかどうか判定するためには実際に値を渡さなければ判定することはできません。
scala> isOptional[Int](1)
val res1: Boolean = false
scala> isOptional[Option[Int]](Some(1))
val res2: Boolean = true
erasedValue
が存在する場合
inline def isOptional[T]: Boolean =
inline scala.compiletime.erasedValue[T] match
case _: Option[?] => true
case _ => false
型レベル関数で評価が行われるので実際に値を渡すことなく型のみで判定を行うことができます。
scala> isOptional[Int]
val res3: Boolean = false
scala> isOptional[Option[Int]]
val res4: Boolean = true
筆者はScala 3のUnion型を使用して、標準の型もしくはそのOption型のどちらかをとる型パラメーターの関数をOSSの開発時に使用しており、Option型であればOption型であるというプロパティを持つようなモデルを作成していました。
しかし、そのモデルは値を持つことはなく型パラメーターのみ受け取るようなものでした。その場合だと以下のように標準の型を受け取ってモデルを生成する関数とOption型を受け取ってモデルを生成する関数を2つ用意しないといけませんでした。
1つや2つであればこれでもよかったのですが、このようなものがいくつもあったり他に型パラメーターだけで判定を行わなければいけない箇所があったので結構同じようなコードが増えて大変でした。
def INT[T <: Int | Long]: Integer[T] = Integer(None, false)
def INT[T <: Option[Int | Long]]: Integer[T] = Integer(None, true)
erasedValue
を知ってからは単純にコードが半分になったり表現できる幅が増えたりとすごく重宝してます。
inline def INT[T <: Int | Long | Option[Int | Long]]: Integer[T] = Integer(None, isOptional[T])
公式サンプルコードのdefaultValue
の方が実際に使用する場面をイメージしやすいと思います。
error/codeOf
error
はインライン展開時にユーザー独自のエラーメッセージを発生させることができるものです。
inline def fail() = scala.compiletime.error("failed for a reason")
呼び出してみると「failed for a reason」という自身で定義したエラーメッセージが確認できます。
scala> fail()
-- Error: ----------------------------------------------------------------------
1 |fail()
|^^^^^^
|failed for a reason
1 error found
constValue
の時に作成した関数にerror
を導入して独自のエラーメッセージを表示してみます。
inline def renameMonth[N <: Int]: String =
inline scala.compiletime.constValue[N] match
case 1 => "January"
case 2 => "February"
case 3 => "March"
case 4 => "April"
case 5 => "May"
case 6 => "June"
case 7 => "July"
case 8 => "August"
case 9 => "September"
case 10 => "October"
case 11 => "November"
case 12 => "December"
これは月にマッチする数字が型パラメーターに渡された時にその英名を返す関数でした。
この関数にパターンマッチにマッチしない定数を渡すとエラーにすることはできましたが、そのエラーメッセージは少しわかりにくいものとなっていました。
エラーメッセージ
scala> renameMonth[13]
-- Error: ----------------------------------------------------------------------
1 |renameMonth[13]
|^^^^^^^^^^^^^^^
|cannot reduce inline match with
| scrutinee: 13 : (13 : Int)
| patterns : case 1
| case 2
| case 3
| case 4
| case 5
| case 6
| case 7
| case 8
| case 9
| case 10
| case 11
| case 12
|----------------------------------------------------------------------------
|Inline stack trace
|- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|This location contains code that was inlined from rs$line$3:2
2 | inline scala.compiletime.constValue[N] match
| ^
3 | case 1 => "January"
4 | case 2 => "February"
5 | case 3 => "March"
6 | case 4 => "April"
7 | case 5 => "May"
8 | case 6 => "June"
9 | case 7 => "July"
10 | case 8 => "August"
11 | case 9 => "September"
12 | case 10 => "October"
13 | case 11 => "November"
14 | case 12 => "December"
----------------------------------------------------------------------------
1 error found
この関数のエラー文をerror
を使用して見やすくします。
パターンマッチングでどれにも一致しない場合にerror
を使用するよう変更を加えます。
inline def renameMonth[N <: Int]: String =
inline scala.compiletime.constValue[N] match
case 1 => "January"
case 2 => "February"
case 3 => "March"
case 4 => "April"
case 5 => "May"
case 6 => "June"
case 7 => "July"
case 8 => "August"
case 9 => "September"
case 10 => "October"
case 11 => "November"
case 12 => "December"
case _ => scala.compiletime.error("月は1 ~ 12までの範囲しかありません。")
定数値を渡してもパターンマッチに存在しない値であった場合コンパイルエラーとなり、独自のエラーメッセージが表示されるようになったことで非常にわかりやすくなりました。
scala> renameMonth[13]
-- Error: ----------------------------------------------------------------------
1 |renameMonth[13]
|^^^^^^^^^^^^^^^
|月は1 ~ 12までの範囲しかありません。
1 error found
error
はこのようにコンパイル時に独自のエラーを表示することができるので、今まで以上に型安全なコードを書けるようになったのではないでしょうか?
少し用途は違うかもしれませんが、Scala 2で使用していたrequire
などはコンパイル時ではなく実行時に例外を吐くだけであったため、定数を取り扱っている場合であればerror
に載せ替えることでより型安全にできるかもしれません。
error
は受け取るメッセージが定数でなければいけません。そのため動的に変わるようなメッセージを出すことができないのです。
先ほどのコードでパターンマッチングに一致しない場合にその値がなんであったかをエラーに含めたい時があると思います。
inline def renameMonth[N <: Int]: String =
inline scala.compiletime.constValue[N] match
case 1 => "January"
case 2 => "February"
case 3 => "March"
case 4 => "April"
case 5 => "May"
case 6 => "June"
case 7 => "July"
case 8 => "August"
case 9 => "September"
case 10 => "October"
case 11 => "November"
case 12 => "December"
case v => scala.compiletime.error(s"$vは1 ~ 12のどれにも一致しません") // このように一致しない値をエラーに含めたい
しかし、error
は受け取るメッセージが定数でなければいけないためこのコードのエラーメッセージは意図したものとはなりません。
scala> renameMonth[13]
-- Error: ----------------------------------------------------------------------
1 |renameMonth[13]
|^^^^^^^^^^^^^^^
|A literal string is expected as an argument to `compiletime.error`. Got v.+("は1 ~ 12のどれにも一致しません")
1 error found
エラーメッセージを動的なものにしたい場合は、codeOf
を使用すると実現することができます。
inline def renameMonth[N <: Int]: String =
inline scala.compiletime.constValue[N] match
case 1 => "January"
case 2 => "February"
case 3 => "March"
case 4 => "April"
case 5 => "May"
case 6 => "June"
case 7 => "July"
case 8 => "August"
case 9 => "September"
case 10 => "October"
case 11 => "November"
case 12 => "December"
case v => scala.compiletime.error(scala.compiletime.codeOf(v) + "は1 ~ 12のどれにも一致しません")
ただcodeOf
は受け取ったコードをそのまま表示する?だけのようなので上記だと受け取ったv
はそのままv
として表示されてしまいます。
scala> renameMonth[13]
-- Error: ----------------------------------------------------------------------
1 |renameMonth[13]
|^^^^^^^^^^^^^^^
|vは1 ~ 12のどれにも一致しません
1 error found
そのため以下のようにすることで求めていたエラーメッセージを表示することができるようになります。
case _ => scala.compiletime.error(scala.compiletime.codeOf(scala.compiletime.constValue[N]) + "は1 ~ 12のどれにも一致しません")
scala> renameMonth[13]
-- Error: ----------------------------------------------------------------------
1 |renameMonth[13]
|^^^^^^^^^^^^^^^
|13は1 ~ 12のどれにも一致しません
1 error found
codeOf
に関しては公式ドキュメントなどでもあまり詳しく記載がなかったのでここら辺の挙動は理解できておりません。
有識者の方がいれば教えていただけると嬉しいです。
ops
scala.compiletime.ops
パッケージには定数値である型に対するプリミティブ演算をサポートするための色々な型が提供されています。
import scala.compiletime.ops.int.*
を使用すれば型レベルでの計算を行うこともできます。
scala> val x: 1 + 2 * 3 = 7
val x: 7 = 7
型レベルによる計算結果とは異なる値を渡した場合はコンパイル時にエラーとなります。
scala> val x: 1 + 2 * 3 = 8
-- [E007] Type Mismatch Error: -------------------------------------------------
1 |val x: 1 + 2 * 3 = 8
| ^
| Found: (8 : Int)
| Required: (7 : Int)
|
| longer explanation available when compiling with `-explain`
1 error found
ops
パッケージにはScalaバージョン3.3.1時点で以下7つのプリミティブ型がサポートされています。
- any
- boolean
- double
- float
- int
- long
- string
パターンマッチングと条件分岐
今まで紹介したScala 3の機能を使用して、コンパイル時にパターンマッチングと条件分岐を実装してみましょう。
試しにMySQLにあるYEAR型が許可する範囲の値のみを受け取ることのできる関数を作成してみます。
要件としては以下を満たすものを実装してみます。(他にも0を受け取れたりもしますが今回は割愛)
- '1901'から'2155'の範囲の4桁の文字列
- 1901 から 2155 までの範囲の4桁の数値
まず数値が渡された場合の条件を実装してみます。
実装自体はとても単純で普段使用するif文と大差ありません。inline
を使用することでコンパイル時に評価ができるようになっただけです。
object DataType:
opaque type Year = Int
inline def year(value: Int): Year =
inline if value >= 1901 & value <= 2155 then value
else error("Only values in the range 1901 to 2155 can be passed to the YEAR type.")
実行してみると意図した通りの挙動になっていると思います。
scala> DataType.year(1901)
val res1: DataType.Year = 1901
scala> DataType.year(2155)
val res2: DataType.Year = 2155
scala> DataType.year(1900)
-- Error: ----------------------------------------------------------------------
1 |DataType.year(1900)
|^^^^^^^^^^^^^^^
|Only values in the range 1901 to 2155 can be passed to the YEAR type.
1 error found
scala> DataType.year(2156)
-- Error: ----------------------------------------------------------------------
1 |DataType.year(2156)
|^^^^^^^^^^^^^^^
|Only values in the range 1901 to 2155 can be passed to the YEAR type.
1 error found
次は文字列が渡された場合の条件を実装してみます。
文字列は4桁の文字であり、かつ数字と同じように1901から2155までの範囲でなければいけません。
以下のように実装を行なってみました。
object DataType:
opaque type Year = String
inline def year(value: String): Year =
inline if """^(19[0-9]{2}|20[0-9]{2}|21[0-4][0-9]|2155)$""".r.matches(value) then value
else error("Only values in the range 1901 to 2155 can be passed to the YEAR type.")
この実装はぱっと見問題なさそうに見えますが、こちらのコード使用すると以下のようなエラーが起きます。
scala> DataType.year("1901")
-- Error: ----------------------------------------------------------------------
1 |DataType.year("1901")
|^^^^^^^^^^^^^^^^^^^^^
|Cannot reduce `inline if` because its condition is not a constant value: augmentString("^(19[0-9]{2}|20[0-9]{2}|21[0-4][0-9]|2155)$").r.matches("1901")
|-----------------------------------------------------------------------------
|Inline stack trace
|- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|This location contains code that was inlined from rs$line$111:4
4 | inline if """^(19[0-9]{2}|20[0-9]{2}|21[0-4][0-9]|2155)$""".r.matches(value) then value
| ^
5 | else error("Only values in the range 1901 to 2155 can be passed to the YEAR type.")
-----------------------------------------------------------------------------
1 error found
正規表現での比較は定数値として扱うことができないため、inline
を使用したコンパイル時評価はできないようです。
筆者も当初できないものとして諦めていましたが、先ほど紹介したscala.compiletime.ops
パッケージを使用すれば正規表現を使用したコンパイル時評価を行うことができるようです。
scala.compiletime.ops
パッケージのstring
にあるMatches
を使用すれば第一引数に渡したString型を第二引数に渡した正規表現に一致するかをコンパイル時にチェックしてくれるようです。
Matches
は単なるBoolean型を返すだけの型レベル関数なので、constValue
を使用することでその型を値として使用し条件分岐を可能にしています。
object DataType:
opaque type Year = String
inline def year(value: String): Year =
inline if constValue[Matches[value.type, """^(19[0-9]{2}|20[0-9]{2}|21[0-4][0-9]|2155)$"""]] then value
else error("Only values in the range 1901 to 2155 can be passed to the YEAR type.")
実行してみると意図した通りの挙動になっていると思います。
scala> DataType.year("1901")
val res1: DataType.Year = 1901
scala> DataType.year("2155")
val res2: DataType.Year = 2155
scala> DataType.year("1899")
-- Error: ----------------------------------------------------------------------
1 |DataType.year("1899")
|^^^^^^^^^^^^^^^^^^^^^
|Only values in the range 1901 to 2155 can be passed to the YEAR type.
1 error found
scala> DataType.year("2156")
-- Error: ----------------------------------------------------------------------
1 |DataType.year("2156")
|^^^^^^^^^^^^^^^^^^^^^
|Only values in the range 1901 to 2155 can be passed to the YEAR type.
1 error found
Year
型だけUnion Typeで共通化して、以下のようにコンパイル時に評価を行える2つのyear
メソッドを実装できました。
object DataType:
opaque type Year = Int | String
inline def year(value: String): Year =
inline if constValue[Matches[value.type, """^(19[0-9]{2}|20[0-9]{2}|21[0-4][0-9]|2155)$"""]] then value
else error("Only values in the range 1901 to 2155 can be passed to the YEAR type.")
inline def year(value: Int): Year =
inline if value >= 1901 & value <= 2155 then value
else error("Only values in the range 1901 to 2155 can be passed to the YEAR type.")
このままでも良いのですが、少し改修を行なって応用にチャレンジしてみましょう。
実際の値を使用せずに、year
メソッドを型のみで実装を行なってみます。
まずyear
メソッドから引数を取り除き、代わりに型パラメーターを渡してあげます。
今回はそれぞれの型でメソッドを作成するのではなく、共通化を行うためUnion Typeを使用してIntもしくはString型を受け取るようにしています。
object DataType:
opaque type Year = Int | String
inline def year[T <: Int | String]: Year = ???
次はまず受け取った型がInt型なのかString型なのかの判定を行う必要があります。
しかし実際の値がなく型しか情報がないので、erasedValue
を使用してあたかも値が存在しているかのように条件分岐を作成してみます。
inline def year[T <: Int | String]: Year =
inline erasedValue[T] match
case _: Int => ???
case _: String => ???
これであとはそれぞれの分岐ごとに処理を追加してあげればうまくいきそうです。
Int型に対してはconstValue[T] >= 1901 & constValue[T] <= 2155
という感じに型TをconstValue
で実際の値として判定を行い、
String型に対してはconstValue[Matches[T, """^(19[0-9]{2}|20[0-9]{2}|21[0-4][0-9]|2155)$"""]]
という感じでMatches
にTの型をそのまま渡してあげれば良さそうです。
以下のような実装になりました。
inline def year[T <: Int | String]: Year =
inline erasedValue[T] match
case _: Int =>
inline if constValue[T] >= 1901 & constValue[T] <= 2155 then constValue[T]
else error("Only values in the range 1901 to 2155 can be passed to the YEAR type.")
case _: String =>
inline if constValue[Matches[T, """^(19[0-9]{2}|20[0-9]{2}|21[0-4][0-9]|2155)$"""]] then constValue[T]
else error("Only values in the range 1901 to 2155 can be passed to the YEAR type.")
しかしこれは機能しません。
上記コードを実装するとコンパイル時に以下のようなエラーが起きてしまいます。
-- [E008] Not Found Error: -----------------------------------------------------
6 | inline if constValue[T] >= 1901 & constValue[T] <= 2155 then constValue[T]
| ^^^^^^^^^^^^^^^^
|value >= is not a member of T, but could be made available as an extension method.
|
|One of the following imports might make progress towards fixing the problem:
|
| import math.Ordered.orderingToOrdered
| import math.Ordering.Implicits.infixOrderingOps
|
|
|
|where: T is a type in method year with bounds <: Int | String
1 error found
-- [E057] Type Mismatch Error: -------------------------------------------------
9 | inline if constValue[Matches[T, """^(19[0-9]{2}|20[0-9]{2}|21[0-4][0-9]|2155)$"""]] then constValue[T]
| ^
| Type argument T does not conform to upper bound String
|
| longer explanation available when compiling with `-explain`
1 error found
これは型Tは、Int型とString型を受け取ることができるUnion TypeでありerasedValue
を使って条件分岐を行なっていますが、条件分岐の先では型Tをそのまま使用しています。
erasedValue
は型Tをあたかも値があるかのように振る舞えるようにできるものであり、型T自体を条件分岐によってInt型である、String型であると保証するものではありません。
実際に値があるわけではないので、asInstanceOf
などを使って無理やり型をキャストすることもできませんし、 値を使用したパターンマッチではないのでcaseの値を使用することもできません。erasedValue
も実体がないため使用するとエラーになってしまいます。
ではどうすればいいのでしょうか?
答えは簡単で、erasedValue
の条件分岐の値は実体がないため使用するとエラーになるのであれば、実体として使用しなければいいだけです。
まずはInt型だと以下のようにcaseの値を実体として使用するのではなく、typeを使用して型として扱います。これでIntの型を取得することができます。
case int: Int => int.type
ただ実体がないためそれぞれの数字との比較ができなくなってしまいました。
これはscala.compiletime.ops
パッケージのint
を使用することで解決できます。
int
には型レベルでの大小比較が提供されているのでそれを使用します。
type Bool = Int >= 1901
// scala> type Bool = 1 >= 1901
// defined alias type Bool = false
これでInt型と1901/2155の定数値の型で比較を行い、Booleanの型を取得することができるようになります。
Booleanの型を取得できたのであとはその型をconstValue
を使用して定数値として扱い、if文の条件分岐として使用してあげれば、実際の値を使用していた時と同じ挙動で実装が行えます。
case int: Int =>
inline if constValue[int.type >= 1901] & constValue[int.type <= 2155] then constValue[T]
else error("Only values in the range 1901 to 2155 can be passed to the YEAR type.")
String型の条件分岐に関しても同様にcaseの値を型として使用すれば、実際の値を使用していた時と同じ挙動で実装が行えます。
case str: String =>
inline if constValue[Matches[str.type, """^(19[0-9]{2}|20[0-9]{2}|21[0-4][0-9]|2155)$"""]] then constValue[T]
else error("Only values in the range 1901 to 2155 can be passed to the YEAR type.")
最終的な実装は以下のようになりました。
object DataType:
opaque type Year = Int | String
inline def year[T <: Int | String]: Year =
inline erasedValue[T] match
case int: Int =>
inline if constValue[int.type >= 1901] & constValue[int.type <= 2155] then constValue[T]
else error("Only values in the range 1901 to 2155 can be passed to the YEAR type.")
case str: String =>
inline if constValue[Matches[str.type, """^(19[0-9]{2}|20[0-9]{2}|21[0-4][0-9]|2155)$"""]] then constValue[T]
else error("Only values in the range 1901 to 2155 can be passed to the YEAR type.")
使用してみると実際の値を使用していた時と同じ挙動で機能していることがわかります。
scala> DataType.year[1901]
val res1: DataType.Year = 1901
scala> DataType.year["2155"]
val res2: DataType.Year = 2155
scala> DataType.year[1900]
-- Error: ----------------------------------------------------------------------
1 |DataType.year[1900]
|^^^^^^^^^^^^^^^^^^^
|Only values in the range 1901 to 2155 can be passed to the YEAR type.
1 error found
scala> DataType.year["2156"]
-- Error: ----------------------------------------------------------------------
1 |DataType.year["2156"]
|^^^^^^^^^^^^^^^^^^^^^
|Only values in the range 1901 to 2155 can be passed to the YEAR type.
1 error found
まとめ
Scala 3で強化されたinline
修飾子を使用して型レベルでパターンマッチと条件分岐を色々できるようになったので、さらに表現力が上がった感じがしますね。
Scala 2だとできなかったことがScala 3だとできるっていうのも増えた気がします。
プロダクト開発だと中々型レベルで機能開発を行うということはほとんどないと思いますが、OSS開発とかだとすごく役立ちそうなので今後面白そうなライブラリや面白い使い方が増えてくるといいなあと思っています。
個人的にはScala 2よりScala 3の方が好きなのでScala 3を書く人がどんどん増えてくれれば良いなと思います。
最近関数型の本が出てサンプルコードでScalaが使われていていたので、そこから関数型だけではなくScalaにも興味を持ってもらえたらなあと思いつつ、Scala 3の記事を色々書いてその人たちや新規の人たちがScala 3面白そうだから触ってみようかなと思えるような記事を書いていきたいなあとも思っています。
今回紹介したinline
は他にtransparent
やsummon
との組み合わせなど色々な用途があるので、是非調べて使ってみてください。
また、もしこんな書き方もできるよであったり、その書き方は非推奨だよなどありましたら教えていただけると幸いです。
参考文献
Discussion