文字列は実質Any問題に対するKotlinのアプローチ 〜TypeScriptのBranded Typesを添えて〜
文字列は実質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に関するベスト記事だと思います。
ここでは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のアプローチは言語レベルでよりスマートな解が用意されていて素晴らしいなと思いました。
Discussion