はじめに
先日ScalaMaturiに初めて参加しました。
各講演どれも勉強になるものばかりで今すぐにでも触ってみたいものばかりだったのですが、今回はその中でもIronというScala3で篩型(ふるいがた)を実現するためのライブラリを触ってみました。
※なお、本記事は当日の発表スライド、公式サイトを参考に書いています。
詳しく知りたい方は公式サイトや発表スライドがかなり詳細に整備されているので、そちらを参考にされることをお勧めします。
あくまでIronを触るきっかけになればという思いで書いているので、ざっくりとした便利な機能のご紹介だと思っていただけると幸いです。
篩型とは
そもそも篩型(ふるいかた)is何って話なのですが、
値の制約を型で表現できるよ
っていうのがざっくりした説明になると思います。
※間違っていたらコメントください・・・
書いてみた
実際にopaqueとIronを使って方に制約をつけたパターンを書いてみます。
opaqueの場合
Scala3のopaqueとスマートコンストラクタを使ってそれっぽいものを書いてみます。
opaque type Name <: String = String
object name:
inline def wrap(input: String): Name = input
extension(value: Name)
inline def unwrap: String = value
//ここで制約を追加する
def parse(input: String): Either[HogeError, Name] =
Either.cond(input.length > 0, wrap(input),
HogeError("1文字以上追加してください")
上記の例に対してそれっぽいものと書いたのは、これは厳密には篩型とは言えないからです。
opaqueは隠蔽型と呼ばれるもので、実際にはStringなどの基本的な型を隠蔽、抽象化しているにすぎません。
なので、何らかの事象で不正なインスタンスが生成された場合にこれらをチェックすることは不可能になります。
また、コンパイル時にエラーも吐いてくれません。
※opaqueはScala2でcase classとか使って値クラス作っていた時と違いnewできないので中々ないケースな気はしますが・・・
Ironの場合
Ironを使って制約を書いてみます。
opaque type Name = String :| MinLength[1]
object Name extends RefinedTypeOps[String, MinLength[1], Name]
Eitherの処理はサボってますが、こんな感じでかけます。
型自身が制約の情報を持てるのと、何よりスッキリ書けるのが個人的に好きです。
また、Ironを使うことでコンパイル時のチェックを行ってくれるのが個人的にはIronを使う大きなメリットに感じています。
Ironをもっと書いてみる
準備
sbtなら以下
libraryDependencies += "io.github.iltotore" %% "iron" % "version"
Millなら以下
ivy"io.github.iltotore::iron:version"
1行で済むのは嬉しいですね。
よく使われるimport
import io.github.iltotore.iron.*
基本の文法
Aが基本の型、CはAに付与された制約を表しています。
IronType[A, C]
//例えば以下は0以上のInt型を表している。
IronType[Int, Greater[0]]
ただ、エイリアスの方がよく使われているみたいです。
val x: Int :| Greater[0]
用意されている制約
いくつかの制約のメソッドが用意されているので、ご紹介します。
進め方ですが、実際に公式サイトを参考にしながらをScala Cliで流していきます。
よければご自身でも試してみてください。
詳細はこちらからご確認ください。
Blank
Stringに対して、空白を許容しないように制約を加えることができます。
//正常系の場合
scala> import io.github.iltotore.iron.*
| import io.github.iltotore.iron.constraint.all.*
|
| val positiveNumber: String :| Blank = "a"
val positiveNumber:
io.github.iltotore.iron.IronType[String,
io.github.iltotore.iron.constraint.all.Blank] = "a"
//異常系の場合
scala> import io.github.iltotore.iron.*
| import io.github.iltotore.iron.constraint.all.*
|
| val positiveNumber: String :| Blank = "a b"
-- Error: ----------------------------------------------------------------------
4 |val positiveNumber: String :| Blank = "a b"
| ^^^^^
|-- Constraint Error --------------------------------------------------------
|Could not satisfy a constraint for type java.lang.String.
|
|Value: "a b"
|Message: Should only contain whitespaces
|----------------------------------------------------------------------------
|----------------------------------------------------------------------------
|Inline stack trace
|- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|This location contains code that was inlined from rs$line$8:4
----------------------------------------------------------------------------
1 error found
記述量少なくていいですね。
Match
渡された文字列の中に任意の文字が含まれていることを確認できます。
//正常系
scala> import io.github.iltotore.iron.*
| import io.github.iltotore.iron.constraint.all.*
|
| val positiveNumber: String :| Match["a"] = "a"
val positiveNumber:
io.github.iltotore.iron.IronType[String,
io.github.iltotore.iron.constraint.string.Match["a"]] = a
//異常系
scala> import io.github.iltotore.iron.*
| import io.github.iltotore.iron.constraint.all.*
|
| val positiveNumber: String :| Match["a"] = "b"
-- Error: ----------------------------------------------------------------------
4 |val positiveNumber: String :| Match["a"] = "b"
| ^^^
|-- Constraint Error --------------------------------------------------------
|Could not satisfy a constraint for type java.lang.String.
|
|Value: "b"
|Message: Should match a
|----------------------------------------------------------------------------
|----------------------------------------------------------------------------
|Inline stack trace
|- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|This location contains code that was inlined from rs$line$10:4
----------------------------------------------------------------------------
1 error found
Divede
渡された数値が任意の数値で割り切れるかチェックできます。
//正常系
scala> import io.github.iltotore.iron.*
| import io.github.iltotore.iron.constraint.all.*
|
| val positiveNumber: Int :| Divide[10] = 1
val positiveNumber:
io.github.iltotore.iron.IronType[Int,
io.github.iltotore.iron.constraint.numeric.Divide[10]] = 1
//異常系
scala> import io.github.iltotore.iron.*
| import io.github.iltotore.iron.constraint.all.*
|
| val positiveNumber: Int :| Divide[10] = 3
-- Error: ----------------------------------------------------------------------
4 |val positiveNumber: Int :| Divide[10] = 3
| ^
|-- Constraint Error --------------------------------------------------------
|Could not satisfy a constraint for type scala.Int.
|
|Value: 3
|Message: Should divide 10
|----------------------------------------------------------------------------
|----------------------------------------------------------------------------
|Inline stack trace
|- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|This location contains code that was inlined from rs$line$12:4
----------------------------------------------------------------------------
1 error found
Greater
任意の数字より大きいことを確認できます。
//正常系
scala> import io.github.iltotore.iron.*
| import io.github.iltotore.iron.constraint.all.*
|
|
| val interval: Int :| Greater[0] = 5
val interval:
io.github.iltotore.iron.IronType[Int,
io.github.iltotore.iron.constraint.numeric.Greater[0]] = 5
//異常系
scala> import io.github.iltotore.iron.*
| import io.github.iltotore.iron.constraint.all.*
|
|
| val interval: Int :| Greater[0] = 0
-- Error: ----------------------------------------------------------------------
5 |val interval: Int :| Greater[0] = 0
| ^
|-- Constraint Error --------------------------------------------------------
|Could not satisfy a constraint for type scala.Int.
|
|Value: 0
|Message: Should be greater than 0
|----------------------------------------------------------------------------
|----------------------------------------------------------------------------
|Inline stack trace
|- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|This location contains code that was inlined from rs$line$14:5
----------------------------------------------------------------------------
1 error found
カスタムエラーメッセージの付与
DescribedAsを使うことで、エラー時に独自のメッセージを付与できます。
scala> import io.github.iltotore.iron.*
| import io.github.iltotore.iron.constraint.all.*
|
| val positiveNumber: Int :| DescribedAs[Greater[0], "The number must be greater than zero"] = 5
これは正しいのでエラーは吐きません。
val positiveNumber:
io.github.iltotore.iron.IronType[Int,
io.github.iltotore.iron.constraint.any.DescribedAs[
io.github.iltotore.iron.constraint.numeric.Greater[0],
"The number must be greater than zero"]
] = 5
制約に反する場合
scala> import io.github.iltotore.iron.*
| import io.github.iltotore.iron.constraint.all.*
|
| val positiveNumber: Int :| DescribedAs[Greater[0], "The number must be greater than zero"] = 0
-- Error: ----------------------------------------------------------------------
4 |val positiveNumber: Int :| DescribedAs[Greater[0], "The number must be greater than zero"] = 0
| ^
|-- Constraint Error --------------------------------------------------------
|Could not satisfy a constraint for type scala.Int.
|
|Value: 0
|Message: The number must be greater than zero
|----------------------------------------------------------------------------
|----------------------------------------------------------------------------
|Inline stack trace
|- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|This location contains code that was inlined from rs$line$6:4
----------------------------------------------------------------------------
1 error found
0より大きいという制約を付与しているため、エラーが発生します。この際事前に定義したエラーメッセージも表示されていることが確認できます。
UnionとIntersection
制約は論理和、論理積を組み合わせることも可能です。
論理和の場合
//正常系
scala> import io.github.iltotore.iron.*
| import io.github.iltotore.iron.constraint.all.*
|
|
| val interval: Int :| Greater[0] | Less[10] = 11
val interval:
io.github.iltotore.iron.IronType[Int,
io.github.iltotore.iron.constraint.numeric.Greater[0]]
| io.github.iltotore.iron.constraint.numeric.Less[10] = 11
//異常系
scala> import io.github.iltotore.iron.*
| import io.github.iltotore.iron.constraint.all.*
|
|
| val interval: Int :| Greater[0] | Less[10] = -1
-- Error: ----------------------------------------------------------------------
5 |val interval: Int :| Greater[0] | Less[10] = -1
| ^^
|-- Constraint Error --------------------------------------------------------
|Could not satisfy a constraint for type scala.Int.
|
|Value: -1
|Message: Should be greater than 0
|----------------------------------------------------------------------------
|----------------------------------------------------------------------------
|Inline stack trace
|- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|This location contains code that was inlined from rs$line$16:5
----------------------------------------------------------------------------
1 error found
パイプ(|)で定義することが可能です。
論理積の場合
//正常系の場合
scala> import io.github.iltotore.iron.*
| import io.github.iltotore.iron.constraint.all.*
|
|
| val interval: Int :| GreaterEqual[0] | LessEqual[10] = 0
val interval:
io.github.iltotore.iron.IronType[Int,
io.github.iltotore.iron.constraint.numeric.GreaterEqual[0]]
| io.github.iltotore.iron.constraint.numeric.LessEqual[10] = 0
//異常系の場合
scala> import io.github.iltotore.iron.*
| import io.github.iltotore.iron.constraint.all.*
|
|
| val interval: Int :| GreaterEqual[0] | LessEqual[10] = -1
-- Error: ----------------------------------------------------------------------
5 |val interval: Int :| GreaterEqual[0] | LessEqual[10] = -1
| ^^
|-- Constraint Error --------------------------------------------------------
|Could not satisfy a constraint for type scala.Int.
|
|Value: -1
|Message: Should be greater than or equal to 0
|----------------------------------------------------------------------------
|----------------------------------------------------------------------------
|Inline stack trace
|- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|This location contains code that was inlined from rs$line$19:5
----------------------------------------------------------------------------
1 error found
&で定義することが可能です。
他にもたくさん便利機能が用意されているので、ぜひ試してみてください。
今回は触れませんが、CirceやZioといった他ライブラリもサポートしているので、今後触ってみようと思います。
個人的に嬉しい点
公式サイト読んでいて個人的にいいなと思った点を雑多にご紹介します。
※本編とはあんまり関係ないので、興味なければ読み飛ばしてください。
実行時のオーバーヘッドが抑えられている点
Ironを使うとコンパイル時に制約のチェックを行うことができるのはもちろんですが、実行時のオーバーヘッドも抑えてくれるみたいです。
//以下の制約は・・・
val x: Int :| Greater[0] = ???
//こんな感じにdesugarされる
val x: Int = ???
上記のように、コンパイル時の制約チェックを行いつつも実行時には基本的な型に戻してくれるため、オーバーヘッドが余計に発生しないような設計になっています。
コンパイル時にチェックが走る
篩型(ふるいがた)を使うことでコンパイル時に型の制約のチェックが走ります。
既述のとおりですが、不正なインスタンスなどの抜け穴を塞いでくれるのがいいですね。
型の振る舞いが直感的になる
型そのものに制約が付与されるので、直感的にVOの振る舞いが把握できる点も嬉しいです。
記述が簡潔
自分で定義するよりもコード量が減る。直感的にかけるのは嬉しいですね。
まとめ
篩型自体初めて聞くレベルの状態でしたが、Ironは難しい記述などを必要とせずに書けるのが採用ハードルを下げているように感じました。
英語ではありますが、公式サイトが詳細に記述してくれているのも嬉しいですね。
ウェブクルーでは一緒に働いていただける方を随時募集しております。
お気軽にエントリーくださいませ。