Kotlinで (Scalaの) 型安全ビルダー

この記事は、[Develop fun!を体現する Works Human Intelligence Advent Calendar 2022](https://qiita.com/advent-calendar/2022/advent-works-hi) 12月24日の記事です。

昨日は @satomihoya さんの記事「[日々息をするようにアウトプットしている人は何を意識しているのか？](https://qiita.com/satomihoya/items/876dea7dd7a4c90c140a)」でした。

よろしければ、ぜひ他の記事もご覧になっていってください。

## はじめに

こんにちは。

みなさんは、**ビルダーパターン**、使っていますか？

https://qiita.com/disc99/items/840cf9936687f97a482b#effective-java-builder

Kotlinには名前付き引数があるためあまり使う機会はありませんが、DSLなどを定義しているときなど、必要になる場合もあります。

```kotlin
fun person(init: PersonBuilder.() -> Unit): Person {
    val builder = PersonBuilder()
    builder.init()
    return builder.build()
}
```

そうなるとやはり気になるのは、必須フィールドの扱いです。典型的なビルダーパターンの実装では、必須フィールドの指定漏れがコンパイルエラーから**実行時例外**になってしまうのです。

それを避ける場合、ビルダーのコンストラクタで必須フィールドを全て受け取る必要があります。
必須フィールドが少ないうちはこれでいいですが、必須フィールドが増えるとビルダーパターンである意味がなくなってきそうです。

```kotlin
val person = PersonBuilder()
    .setAge(128)
    .build() // IllegalStateException: The name field is required

val person = PersonBuilder("John") // What about data classes with many required fields?
    .setAge(128)
    .build()
```

そこで一工夫して、必須フィールドを**静的に**チェックできるようにします。

このパターンは、Scalaでは型安全ビルダーとして知られています。

なお、Kotlin の公式ドキュメントで書かれている Type-safe builders とは全くの別物です。

https://kotlinlang.org/docs/type-safe-builders.html

## Type-safe builder by phantom types

まずは、ビルダーによって生成する型を紹介します。

この`Person`データクラスは、`name`フィールドが必須フィールドで、`age`フィールドがオプショナルなフィールドです。

```kotlin
data class Person(val name: String, val age: Int?)
```

次に、ビルダーを作成します。

型安全ビルダーの特徴は、型引数に状態を表す **Phantom type** を保持するということです。

```kotlin
sealed class Status {
    class Ready    : Status()
    class NotReady : Status()
}

data class PersonBuilder<HasName : Status>(
    val name: String? = null,
    val age: Int? = null,
)
```

保持する状態とは、すなわち`build`を呼び出せる条件が整っているかどうかです。

次のように、`build`関数を全ての条件が満たされた時にのみ呼び出せるようにします。

```kotlin
fun PersonBuilder<Status.Ready>.build(): Person = Person(name!!, age)
```

そして、ビルダーを作成した時点では準備が整っていないように設定し、特定のメソッドを呼び出すと特定の条件が満たされるようにします。この場合、`setName` を呼び出すと、 `HasName` 型パラメータに `Status.Ready` が入ります。

条件に関与しないメソッドは条件を変更しません。

```kotlin
fun mkPersonBuilder() = PersonBuilder<Status.NotReady>()

fun PersonBuilder<*>.setName(name: String): PersonBuilder<Status.Ready> {
    @Suppress("unchecked_cast") // Casts only phantom types
    return copy(name = name) as PersonBuilder<Status.Ready>
}

fun <S : Status> PersonBuilder<S>.setAge(age: Int): PersonBuilder<S> {
    return copy(age = age)
}
```

これだけで一連の道具が揃いました。ビルダーとして利用してみましょう。

次のコードはコンパイルが通ります。

```kotlin
    val john = mkPersonBuilder()
    	.setName("John")
        .setAge(128)
        .build()
    println(john) // Person(name=John, age=128)
```

次のコードはコンパイルが通りません。

```kotlin
    val john = mkPersonBuilder()
    	// .setName("John")
        .setAge(128)
        .build()
    println(john)
```

```
Unresolved reference. None of the following candidates is applicable because of receiver type mismatch
```

## おわりに

簡単にScalaの型安全ビルダーをほとんどそのまま移植することができました。
シンプルかつ柔軟で、とても良いイディオムですね。

強いて言えば、`setter`がビルダーの型を変えるため、このビルダーをDSLに組み込んだとしても [Type-safe builders](https://kotlinlang.org/docs/type-safe-builders.html) として紹介されているような使い心地とは違ってしまうところでしょうか。

```kotlin
person { builder ->
    builder
        .setName("John")
        .setAge(128)
}
```

型引数で状態を管理する代わりにインターフェースで状態を管理して、setterを実行するとスマートキャストが行われるようにする手もあります（参考: https://pl.kotl.in/2f7hlXKmS ）が、こちらはこちらで色々と面倒くさいです。

コード全文は以下にあります。

https://pl.kotl.in/fH_4y-Owg