← melang

Kotlin

melangの各noteで他言語と比較した、Kotlinの設計をまとめたページ。ここに書かれている内容は、各noteの比較表から自動で集めたものです。

型

型推論が強く、Null安全が型に組み込まれている。

val age: Int = 20
val age: Int = 20
val name = "Alice" // 型推論で String
var nickname: String? = null // Nullを許容する型は明示的に分ける

型推論が強力で、型注釈を省略した書き方が主流。`String`と`String?`のようにNull許容/非許容を型システムで区別するのが特徴。

型注釈の位置

postfix構文で、戻り値は式本体で定義した場合に限り推論できる。

val age: Int = 20
val age: Int = 20
val name = "Alice" // 型推論でString

fun greet(name: String) = "Hello, $name" // 式本体なら戻り値の型を省略できる
fun greet2(name: String): String { return "Hello, $name" } // ブロック本体は明示が必要

`name: Type`という一貫したpostfix構文。引数の型注釈は必須で推論できないが、戻り値の型は式本体(`= expr`)で定義した関数に限り推論できるという、本体の書き方によって推論範囲が変わる設計。

型変換

変換をメソッドで書き、失敗し得るものにはOrNullを付ける。

val y = x.toLong()
val x: Int = 10
val y: Long = x.toLong()

val big: Long = 3_000_000_000
val z: Int = big.toInt() // 黙って切り捨てられる

val a: Int? = "42".toIntOrNull()   // 失敗するとnull
val b: Int = "abc".toInt()          // NumberFormatException

数値変換は`toLong()`, `toInt()`のようなメソッドで書き、melangの記法と最も近い。文字列のパースには例外を投げる`toInt()`と、失敗時にnullを返す`toIntOrNull()`があり、名前で失敗の扱いを区別している。ただしnullは失敗の理由を持たず、`Long.toInt()`のような数値の縮小変換は失敗を検出しない。

暗黙の型変換

拡大変換であっても数値型間の暗黙変換はない。

val x: Int = 10
val x: Int = 10
val y: Long = x          // コンパイルエラー
val y: Long = x.toLong() // OK

Javaと異なり、Kotlinは拡大変換であっても数値型間の暗黙変換を行わない。`toLong()`, `toDouble()`のような明示的な変換メソッドを必ず呼ぶ設計で、melangが目指す方針に最も近い言語の一つ。

Enum / Sum Type

単純な列挙とADTAlgebraic Data Type(代数的データ型)の略。複数の型を組み合わせて新しい型を作る手法で、「AまたはB」のように取りうる形が閉じた集合になっているものをSum Type(直和型)と呼ぶ。Rustの`enum`やHaskellの`data`が代表例で、各variant(構成要素)が異なるフィールドを持てる点で、単純な整数の列挙とは異なる。を、enum classとsealed classで役割分担する。

sealed class Result
data class Success(val user: User) : Result()
sealed class Result
data class Success(val user: User) : Result()
data class Failure(val message: String) : Result()

fun describe(r: Result): String = when (r) {
    is Success -> "ok: ${r.user}"
    is Failure -> "error: ${r.message}"
} // whenがsealedを網羅していないとコンパイルエラー

単純な列挙は`enum class`、payload付きのADTAlgebraic Data Type(代数的データ型)の略。複数の型を組み合わせて新しい型を作る手法で、「AまたはB」のように取りうる形が閉じた集合になっているものをSum Type(直和型)と呼ぶ。Rustの`enum`やHaskellの`data`が代表例で、各variant(構成要素)が異なるフィールドを持てる点で、単純な整数の列挙とは異なる。は`sealed class` / `sealed interface`と役割を分ける。`when`式はsealed階層を網羅しないとコンパイルエラーになり、enumのwhenでも同様に網羅性を検査する。

値の不変性

`val`は束縛を固定するが、参照先が不変とは限らない。

val x = ... / var x = ...
val tags = mutableListOf("a")
// tags = mutableListOf("b")  // コンパイルエラー: valは再代入不可
tags.add("admin")              // OK: MutableListは変更できる

val ro: List<String> = tags    // 読み取り専用のビュー
// ro.add("x")                 // コンパイルエラー
tags.add("y")                  // roからも見える

data class User(val name: String)
val renamed = User("alice").copy(name = "bob")

`val`と`var`を使い分け、`val`を基本とするのが慣習である。`List`は読み取り専用のインターフェースで、実体が`MutableList`の場合は別の参照から変更でき、不変が保証されるわけではない。`data class`の`copy`で、新しい値を作る書き方ができる。

名前付き引数

名前付き引数と位置引数を併用でき、デフォルト引数と組み合わせてよく使われる。

fun foo(width: Int, height: Int = 0)
fun foo(width: Int, height: Int = 0) {}
foo(width = 10, height = 20) // 名前付き
foo(10, height = 20)         // 位置引数と併用可
foo(height = 20, width = 10) // 順序は自由

名前付き引数と位置引数を併用でき、名前付きにすれば順序も自由に変えられる。デフォルト引数と組み合わせることで、オプションが多い関数を簡潔に呼び出せる。

関数の引数

引数は再代入できない。渡し方を選ぶ構文はない。

fun f(n: Int, list: MutableList<Int>)
fun f(n: Int, list: MutableList<Int>) {
    // n = 1     // 引数は再代入できない(コンパイルエラー)
    list.add(1) // 渡されたオブジェクトの中身は変更できる
}

引数は常に`val`相当で再代入できないため、引数自体を書き換えて呼び出し側に影響を与える事故は防げる。一方で、可変なコレクションを渡せば中身は変更できる。melangの「代入そのものを禁止する」方向と近く、引数を共有して渡す点も同じ。ただしmelangは、オブジェクトの中身も不変にする。

if 文/式

`if`は式。値として使うときは`else`が必須で、`else`なしの`if`はstatementとしてのみ使える。

val x = if (cond) a else b
val x = if (cond) 1 else 2

// 値として使うならelseが必要
// val y = if (cond) 1   // コンパイルエラー

if (cond) {
    process()
}

値として使うときだけ`else`が必須になる点が、melangの設計に近い。一方で、手続き的な`if ... else`も許可されているため、`else`の用途は制限されていない。

I/O の明示

`suspend`修飾子が伝播し、非suspend関数からは呼べない。

suspend fun findUser(id: UserId): User
suspend fun findUser(id: UserId): User { /* ... */ }

fun getUser(id: UserId): User {
    findUser(id) // NG: suspend関数はsuspend文脈でしか呼べない
}

suspend fun getUser(id: UserId): User {
    findUser(id) // OK
}

`suspend`はコルーチンの中断可能性を示すマーカーで、I/O専用ではないが、多くのI/O処理がsuspend関数として書かれる。呼び出し元への伝播が強制される点は`io`ときわめて近いが、表現しているのは「中断されうるか」であって「I/Oを行うか」ではなく、同期的なI/O(`File.readText()`など)はsuspendなしで呼べてしまう。

言語名

フィンランドの島の名前に由来し、既存語との衝突がない固有名詞。

開発元JetBrains本社近くの島`Kotlinsaari`(コトリン島)に由来する。一般語ではないため検索性・一意性が高く、大文字・小文字の表記揺れも起きにくい。地名由来の命名は、意味を持たせずに固有の識別子を得る手段として機能する。