Kotlin 2.1からのwhen式のGuard Conditionsに入門する

## 使用条件

現時点ではpreviewの機能なので、Kotlin 2.1に上げるだけでは使えません。
以下の手順で使えるようになります。

1. Android Studio等でK2 modeをonにする
2. build.gradleに以下を追加
```gradle
kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xwhen-guards")
    }
}
```

## Guard Conditionsとは

Guardとは警備員、みたいな意味で、条件に満たすかどうかを見張る役割があります。
Kotlin2.1で追加されたwhen式のGuard Conditionsの機能により、追加の条件を`if`によって書くことができます。

下記のようなクラスを考えます。

```kotlin
enum class ErrorCode {
    NOT_FOUND,
    UNKNOWN
}

sealed interface Result {
    data object Success : Result
    data class Error(val code: ErrorCode) : Result
}
```

このresultをwhen式で判定します。これは従来のやり方です。

```kotlin
when (result) {
    Result.Success -> println("Success")
    is Result.Error -> {
        when (result.code) {
            ErrorCode.NOT_FOUND -> println("Error: NOT_FOUND")
            ErrorCode.UNKNOWN -> println("Error: UNKNOWN")
        }
    }
}
```

Guard Conditionsを使うと、以下のように書けます。


```kotlin
when (result) {
    Result.Success -> println("Success")
    is Result.Error if (result.code == ErrorCode.NOT_FOUND) -> println("Error: NOT_FOUND")
    is Result.Error -> println("Error: UNKNOWN") // これはelse -> でも可
}
```

Guard Conditionsを使ってwhen式を書くメリットは下記の通りです。
- when式の条件分岐を簡潔にし、可読性を向上させる
- スマートキャストが働いているので、Result.Errorと判定した後に、codeプロパティにアクセスできる
- whenの網羅性が働くので、例えば以下のとき、IDE上でエラーを出してくれる

```kotlin
// コンパイルエラー
when (result) {
    Result.Success -> println("Success")
    is Result.Error if (result.code == ErrorCode.NOT_FOUND) -> println("Error: NOT_FOUND")
}
```

見落としているケースがあればエラーを出してくれるので間違いに気づけます。

しかし反面、Guard Conditionsの網羅性は検知しないパターンがあり、以下のような条件を網羅されているコードでもエラーとなってしまうことに注意です。

```kotlin
// 網羅しているのにコンパイルエラーとなってしまう
when (result) {
    Result.Success -> println("Success")
    is Result.Error if (result.code == ErrorCode.NOT_FOUND) -> println("Error: NOT_FOUND")
    is Result.Error if (result.code == ErrorCode.UNKNOWN) -> println("Error: UNKNOWN")
}
```

ちなみに、Guard Conditionsのifの条件は、引数に関係なくてもよいです。関係ある場合はスマートキャストが働きますが、別に関係なくてもよいです。

```kotlin
when (result) {
    Result.Success -> println("Success")
    is Result.Error if Math.random() > 0.5 -> println("Error but happy")
    else -> println("Error and unhappy")
}
```

## else ifを使う

また、else ifを使うこともできます。
これによって、引数の値を評価することなしに、追加の条件をいきなり使うことができます。

```kotlin
// 別途拡張関数を定義
fun Result.isKnown(): Boolean = when (this) {
    Result.Success -> true
    is Result.Error -> this.code != ErrorCode.UNKNOWN
}

fun main() {
    // 略

    when (result) {
        Result.Success -> println("Success")
        else if result.isKnown() -> println("Error: KNOWN")
        else -> println("Error: UNKNOWN")
    }
}
```

先にResultのどちらの型か判定せずとも、Guard Conditionsで条件を判定することができていろいろ使えそうですね。

まぁこの条件だと使う意味は薄いですが、型が増えたり条件が複雑になってくると使えます。

たとえばこんな感じでしょうか。

```kotlin
sealed interface Status {
    data object Loading : Status
    data class Success(val message: String) : Status
    data class Error(val code: ErrorCode) : Status
}

fun Status.hasNoMessage(): Boolean = when (this) {
    Status.Success -> this.message.isEmpty()
    else -> true
}

fun main() {
    // 略

    when (status) {
        Status.Loading -> println("Loading")
        else if status.hasNoMessage() -> println("Error: NO_MESSAGE")
        else -> println("Has message")
    }
```

まぁうまい例を出せたかどうかはわかりませんが、複雑な条件を使うときに良さそうです。

なお、else ifの場合も、引数に関係ない条件も入れられます。


## まとめ

Guard conditionsの用途として、sealed classを使って複雑な条件を使う、上記のような場合に効力を発揮します。
まだpreviewなので、現時点で使うには設定が必要ですし、最終的に取り入れられるかは様子見ですが、
いい感じなのでデフォルト機能になってくれることに期待です。

## おまけ

Guard conditionsの構文として、最初は`&&`が提案されていたが、問題が生じて、`if`に変更されたとのこと。

[Guards Alternative syntax using &&](https://github.com/Kotlin/KEEP/blob/guards/proposals/guards.md#alternative-syntax-using-)

問題とは、`&&`を使うと、以下のような書き方が同じ評価になってしまう。

```kotlin
is Status.Success && info.isEmpty || status is Status.Error
// would be equivalent to
is Status.Success && (info.isEmpty || status is Status.Error)
```

直感的に見ると最初の`&&`が評価される用に見えるが、Guard conditionsがひとまとまりだから、`&&`後の`info.isEmpty || status is Status.Error`の条件が先に評価される。これは直感に反しているので、やめたということです。

[こちらのXのポスト](https://x.com/at_sushi_at/status/1890378289086124541)で上記知ったのですが、[Kotlin/KEEP: Kotlin Evolution and Enhancement Process](https://github.com/Kotlin/KEEP)のリポジトリを見ると、Kotlinの機能の提案や採択理由が見れるので、面白いですね。

## おまけのおまけ

新機能と関係ないですが、whenでenumかsealed classを使うとき、isがいるかどうかです。

オブジェクトの場合は一致しているかどうかなので、isがいらないです。enumかdata objectはオブジェクトです。

data classは型を判定しているので、isが必要です。

```kotlin
// enum class
val errorCode: ErrorCode
when (errorCode) {
    ErrorCode.NOT_FOUND -> println("Error: NOT_FOUND")
    else -> println("Error: UNKNOWN")
}

// sealed class
val status: Result
when (status) {
    // data object
    Result.Success -> println("Success")
    // data class
    is Result.Error -> println("Error: ${status.code}")
    else -> println("Success")
}
```

上記の`ErrorCode`と`Result`のクラス定義は[こちら](#guard-conditionsとは)

## 追記(2025/3/24)

ktlintを使っている場合は、guard conditionsが引っかかってしまいます。

下のissueだと、こんな感じのエラーが出ます。

```kotlin
Step 'ktlint' found problem in 'src/main/kotlin/com/hippo/ehviewer/spider/ComicInfo.kt':
79:18 Expecting '->'
```

この場合、ktlintのバージョンを1.5.0に上げると良いようです。


https://github.com/pinterest/ktlint/issues/2809

ktlintのgradleはデフォルトで1.1.0なのでバージョンを手動で指定してやる必要がありますが、プルリクが上がっているので時間の問題で解決かと思います。

https://github.com/JLLeitschuh/ktlint-gradle/pull/818/files

## 参考
[Kotlin 2\.1\.0: Smarter when with Guard Conditions \| by Tom Sabel \| Medium](https://tomsabel.com/kotlin-2-1-0-smarter-when-with-guard-conditions-4541160d67a0)

[Kotlin 2\.1\.0の新しい言語機能Preview\. Kotlin… \| by Kenji Abe \| Dec, 2024 \| Medium](https://star-zero.medium.com/kotlin-2-1-0%E3%81%AE%E6%96%B0%E3%81%97%E3%81%84%E8%A8%80%E8%AA%9E%E6%A9%9F%E8%83%BDpreview-360ec5bc812c)

[Conditions and loops \| Kotlin Documentation](https://kotlinlang.org/docs/control-flow.html#guard-conditions-in-when-expressions)