🫖

文字列は実質Any問題に対するKotlinのアプローチ 〜TypeScriptのBranded Typesを添えて〜

2024/12/30に公開

文字列は実質Any

例えば従業員IDを管理するemp_123456_idと部署IDを管理するdep_123456_idがあるとします。

これを文字列として扱った場合、コードとしては以下のようになります。

val employeeId = "emp_123456_id"
val departmentId = "dep_123456_id"

ですが、これらは文字列のためこのように従業員IDと部署IDを間違って代入できてしまいます。

val employeeId = "dep_123456_id"
val departmentId = "emp_123456_id"

これは両方とも文字列のため、コンパイラは従業員IDと部署IDを区別できないからです。

TypeScriptのBranded Types

この「文字列は実質Any」という悩みは静的型付け言語ではだいたい発生する問題です。

例えばTypeScriptの場合は、Branded Typesというコーディングパターンがベストプラクティスとして知られています。個人的にはうひょさんの以下の記事が今のところBranded Typesに関するベスト記事だと思います。

https://qiita.com/uhyo/items/de4cb2085fdbdf484b83

ここではBranded Typesについて詳しくは説明しませんが、上記の記事のうひょさんのコードを拝借するとこんな感じ。

const userIdBrand = Symbol();

export type UserId = string & { [userIdBrand]: unknown };

export function createUserId(rawId: string): UserId {
  return rawId as UserId;
}

簡単に言えばモジュール内でユニークなsymbolを持ったUserIdという型をpriveteに作り、文字列→UserId型へconvertする関数を公開することでUserIdという型を付けるという方法です。

「文字列は実質Any問題」に対するKotlinのアプローチ

ではKotlinの場合はどのように解決するのか?

「さぞコードをこねくり回すんやろなぁ…」と思った方、安心してください。以下の2行で終了します。[1]

@JvmInline
value class UserId(private val id: String)

あとはこのように使うと、文字列のユーザーIdはUserIdとして扱うことができ、コンパイラが怒ってくれるようになります。

fun printUserId(userId: UserId) {
    println("User ID is: ${userId.id}")
}

val userId = UserId("12345")
printUserId(userId) // "User ID is: 12345"

ただし、

  • プライマリコンストラクタは単一のプロパティしか持てない
  • 他のインターフェースやクラスを継承できない

といった、いくつかの制約があります。

以上で本筋の話は終わりなんですが、もう少しinlineというものについて補足していきます。

inlineとは?

「そもそもinlineってなに?」という話ですが、特定のコードがコンパイル時にその場に展開されることです。

クラスに限らずinlineは関数などでも有効になります。

これはbefore/afterでコードを見比べた方がイメージが湧きやすいです。

インライン化されないクラス
class UserId(val id: String)

fun main() {
    val userId = UserId("12345") // オブジェクトが生成される
    println(userId.id)
}
インライン化されたクラス
@JvmInline
value class UserId(val id: String)

fun main() {
    val userId = "12345" // コンパイル時にはこれと同じような状態になる
    println(userId)
}

関数の場合はこんなイメージ。

インライン化されない関数
fun greet(name: String): String {
    return "Hello, $name!"
}

fun main() {
    println(greet("Alice")) // 関数呼び出しが発生
}
インライン化された関数
inline fun greet(name: String): String {
    return "Hello, $name!"
}

fun main() {
    println("Hello, Alice!") // コンパイル時にはこれと同じような状態になる
}

インライン化のメリデメ

万事よさそうなインライン化のように思えますが、いくつかデメリットもあります。

最後に、メリット・デメリットを整理しておきましょう。

メリット

パフォーマンス向上

  • 関数呼び出しのオーバーヘッドが削減される
  • 不要なインスタンス生成を回避できる

メモリ効率

不要なインスタンスを生成しないため、ガベージコレクションの負担が軽減される。

型安全性(インラインクラスの場合)

型チェックがコンパイル時に行われるため、意図しない型の使用を防ぎます。

デメリット

コードサイズの増加

インライン化によってコードが展開されるため、大量に使用するとバイナリサイズが大きくなる可能性があります。

無条件に使うべきではない

簡潔で頻繁に使われる小さなコードに適していますが、複雑で長いコードをインライン化すると逆効果になることがあります。

おわりに

TypeScriptの型を学んでいくうちに、「これKotlinならどうなんだろう?」というのを最近は繰り返しています。(逆もまた然り)

今回はBranded Typesがテーマだったんですが、これに関してはKotlinのアプローチは言語レベルでよりスマートな解が用意されていて素晴らしいなと思いました。

脚注
  1. https://kotlinlang.org/docs/idioms.html#use-inline-value-classes-for-type-safe-values ↩︎

Discussion