Lambdaカクテル

京都在住Webエンジニアの日記です

Invite link for Scalaわいわいランド

Scalaの篩型ライブラリ Iron v3.0.0 がリリースされた / 構文がより簡潔になり、扱いやすく、強力に(ただし Scala 3.6.3 が必要)

Scala 3 には篩型(refinement types)を実現するためのライブラリであるIronが存在する。

blog.3qe.us

このライブラリを使うと、非負整数とか、8桁の文字列とか、UUIDといった、元々の型に加えて制約を加えたものをそのまま型情報に載せて扱うことができる。詳しくは記事を読んでみてほしい。

↑の記事にも追記したことだが、先日 Iron v3.0.0 がリリースされた。おめでとう!

github.com

この記事では、ほぼ上に書かれていることをなぞっているだけだが、 Iron 3 の変更について紹介する。

また、公式にマイグレーションガイドが提供されている。

iltotore.github.io

Iron 3

見ての通り Iron 3 は Iron の3つめのメジャーバージョンであり、いくつかの破壊的変更を含んでいる。しかしその差異は小さく軽微で、十数分もあれば訂正できてしまうようなものばかりだ。 それどころか、いくつかの型定義が簡略化されており、より簡単に篩型を定義できるようになっている。順に見ていこう:

破壊的変更: Scala 3.6.3 以上が必要になった

コンパイラのバグに影響を受けていた箇所があったようで、これが修正された Scala 3.6.3 以降でなければ Iron 3 は動作しない。

Ironのリリースポリシーは「可能な限りLTSに留まる」というものだが、メジャーバージョンアップにともなっていったん最新のScalaを利用しなければならなくなったようだ。このバグ修正が含まれた次回のLTSが発表されたタイミングで、IronがLTSにまた留まることになるはずだ。

破壊的変更: RefinedTypeを作成する手順の簡略化

従来のIronには、独自の篩型定義にapplyメソッドを付けて一般的なコンテナ型のように扱えるようにする機能が存在していた(とりあえずRefinedTypeと呼んでおく)。例えばKelvinという篩型に対して、以下のような記述ができるようになる:

Kelvin(300.0)

この機能は従来はRefinedTypeOpsによって提供されていた:

import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.numeric.* // for Positive

opaque type Kelvin= Double :| Positive
object Kelvin extends RefinedTypeOps[Double, Positive, Kelvin]

Iron 3.0.0 以降は、RefinedTypeを介することになる:

import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.numeric.* // for Positive

type Kelvin  = Kelvin.T
object Kelvin extends RefinedType[Double, Positive]

ここで3つの変更点が生じている:

  • opaque type宣言はもはや必要なく、typeを利用するだけでよい。
  • type宣言はもはやobjectTを呼ぶだけでよい。
  • object側ではRefinedTypeを継承し、元々の型と制約の2つを記述するだけでよい。

この変更は軽微な一方、制約の定義が複雑な場合に威力を発揮する:

// 従来
type KelvinR = DescribedAs[Positive, "Kelvin should be positive"]
opaque type Kelvin= Double :| KelvinR
object Kelvin extends RefinedTypeOps[Double, KelvinR, Kelvin]

// 3以降
type Kelvin = Kelvin.T
object Kelvin extends RefinedType[Double, DescribedAs[Positive, "Kelvin should be positive"]]

一度中間にopaque typeを挟む必要がなくなったため、制約(ここではKelvinR)の定義が複雑化しても素直に書けているのが分かるはずだ。

RefinedType#apply は何の断りもなく呼び出せるようになった

先程も説明したRefinedTypeだが、利用するときには特殊なインポートを入れる必要があった:

//Double を Double :| Positive に変換するために必要
import io.github.iltotore.iron.autoRefine

Kelvin(600.0)

しかしこれはライブラリ作者にとって不都合だった。というのもライブラリが使っている Iron の事情がアプリケーションコードにはみ出してしまうからだ。使う側からすれば、よくわからない import が増えてしまうので嬉しくないし分かりにくい。

Iron 3からは、この import はまったく不要になった:

Kelvin(600.0)

破壊的変更: カスタム制約の定義にinlineが必要になった

Iron では、既存の制約で足りない場合に自前で制約を定義できる:

// 従来のコード
final class IsDNABase

given Constraint[Char, IsDNABase] with
  override inline def test(value: Char): Boolean = value == 'A' || value == 'T' || value == 'G' | value == 'C'
  override inline def message: String = "Should be one of A, T, G, C"

Iron 3 からは、Constrainttestメソッドの引数をinlineで定義しなければならなくなった:

final class IsDNABase

given Constraint[Char, IsDNABase] with
  // inline value: Char になっている
  override inline def test(inline value: Char): Boolean = value == 'A' || value == 'T' || value == 'G' | value == 'C'
  override inline def message: String = "Should be one of A, T, G, C"

非-プリミティブな型もコンパイルタイミングでチェックできるようになった

Iron の面白い特徴として、リテラルはコンパイルタイミングでチェックしてもらえる、というものがある:

val k: Kelvin = Kelvin(-42.0) // コンパイルタイミングで弾かれる

他の多くのライブラリでは、リテラルであっても実行時にいったんバリデーション処理を通さなければならないことが多い:

import io.github.iltotore.iron.*

// 極端な例
val k: Option[Kelvin] = -42.0.refineOption[Kelvin]

しかし、このコンパイルタイミングでチェックできるのはプリミティブな値、つまりただの文字列とか整数などに限られていた。

Iron 3 からはこの制約が一部緩和され、以下の型のリテラルがコンパイルタイミングで検証してもらえるようになった:

  • BitInt
  • BigDecimal
  • Array
  • List
  • Set

例えば、偶数のIntしか入らないSetであるEvenSetを定義した場合、以下のようなリテラルがコンパイルタイミングでチェックされる:

import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.collection.ForAll
import io.github.iltotore.iron.constraint.numeric.Even

type EvenSet = Set[Int] :| ForAll[Even]

val es: EvenSet = Set(42)
val ex: EvenSet = Set(41) // コンパイルしない

ちなみにこの改善は、前述したinlineを導入した副産物であるとのこと。面白い。

エラーメッセージの簡略化

これまで型チェックに失敗したエラーメッセージはけっこう長かったが、Iron 3 からはより簡潔に表示されるようになった。

PureConfig への標準対応

設定ファイル読み込みライブラリである PureConfig を扱うためには別途ライブラリを導入する必要があったが、 Iron 3 からは何もせずとも対応するようになった。

★記事をRTしてもらえると喜びます
Webアプリケーション開発関連の記事を投稿しています.読者になってみませんか?