🐷

他言語からKotlinへ入ってきた人に向けて質問に答えてみる

2024/03/24に公開

戻り値のUnitとvoidの違いは?

Kotlinは実は、Scalaの影響を受けています。Unit型もその一つです。なにかしら値を返すものが関数なので、その原則に従っている点がvoidとの違いです。ScalaのUnit型は、以下のように説明されています。

https://docs.scala-lang.org/ja/tour/unified-types.html

Unitは意味のある情報をもたない値型です。Unit型のインスタンスはただ1つだけあり、()というリテラルで宣言することができます。 全ての関数は必ず何かを返さなければなりません。そのためUnitは戻り値の型として時々役立ちます。

Unitとvoidの違いが出る具体的な場所で言うと、例外が投げられたり、無限ループなどです。この場合、Unitではなく、Nothingが使われます。
Scalaのドキュメントによる説明は以下の通りです。

Nothingは全ての型のサブタイプであり、ボトム型とも呼ばれます。Nothing型を持つ値は存在しません。 一般的に例外のスロー、プログラム終了、無限ループなど終了していないことを示すのに使われます。 (すなわち、値として評価されない式や正常に返らないメソッドなどです。)

JavaやTypeScriptにあったvoidは、手続き型プログラミング的な発想で、もともとはC言語の文法だったりします。

TypeScriptにも型があるけど、Kotlinの型とは何が違うの?

なんか実行時にも型情報があるっぽいぞ?

はい、そうです。このため、外部から受け取った情報でも安全性が高いです。
TypeScriptは、実行時には型情報を持っていない。TypeScriptの型は、ソースコード上だけの型です。JavaやKotlinではは、実行時にも型情報があるので、外部から受け取った情報でも安全性が高いです。
また、クラスや関数の定義情報が実行時にも受け取れるので、リフレクションで型情報を元に処理をするにしても、安全性が高いです。リフレクションでも、型情報にあるものしか読み取らないため、意図しない型のオブジェクトを読み取るということが発生しにくいです。

TypeScriptと違って、anyにして騙すことができないね…

Kotlinでは(Javaもですが)、型情報にあるものしか実行できないのです。なのでAnyにすると、Anyから生えているメソッドしか使えないです。

TypeScriptのanyは、nullを代入できるけど、KotlinのAnyはnullを代入できないのはなぜ?

KotlinでのNullableは、すべての型にそれぞれ生えているスーパータイプなので、String?はStringのスーパータイプ、Any?はAnyのスーパータイプですね。
逆に、TypeScriptのanyだとなんでnullが代入可能なのかというと、TypeScriptのanyは任意の型(実質的に、型を無視する)という意味なので、anyにnullを代入ができる。
KotlinのAnyはAny?を除く全ての型の親玉という意味なので、意味が違います。
※ちなみに、Scalaでは、None(Scalaで値がないことを示す値)もオブジェクトであり、Anyは全ての型のスーパータイプなので、AnyにNoneを代入できます。

ユニオン型を使いたいんですが…

sealed classかenumを使いましょう。これらを使うと、when式などで、全てのパターンを網羅しているかをコンパイラがチェックしてくれます。
Kotlinは、Javaのクラスベースという設計を受け継いでいるため、このようなclassやenumを使う仕様となっています。

arrayOfとlistOfの違いは?

listOfやmutableListOfは、ArrayList型が主に使われます。Listを配列を使って実装したものです。
arrayOfで生成される型は、Array<E>で、その実体は、配列です。
配列に関して、C言語やC言語の名残がやや残っているJavaをやっていた人なら分かるかとは思いますが、配列は、連続したメモリ領域をプログラムで扱えるようにしたものです。配列は、シーケンシャルアクセスや、ランダムアクセスが高速ですが、サイズを変更することはできません。
KotlinではJavaと違い、Arrayというクラスになっているため、Listというクラスと似たようなものと感じるかもしれませんが、内部が異なるのです…。
ListのサブタイプにLinkedListというものもあります。Listの実装に配列を使わずに、各要素をポインタでつないでいるものです。ランダムアクセスは遅いですが、挿入や削除がポインタの変更だけで済むので高速です。

こういったインターフェイスが似たまたは同じ型で、実装が違うデータ構造が、Java/Kotlin/Scalaでは度々ありますが、これらを使い分けることで、プログラムの処理効率を上げることができます。

Pythonにもdataclassがあるよね

はい。そうです。Kotlinが影響を受けたScalaの時点から、関係がありそうな(どれぐらい関係があるかは保証しない)できことについて説明します。

2003年に登場したScalaがHaskellのパターンマッチのようなことをやりたくて、case classを作った。データのパターンに応じて処理するという意味でcase。
2016年にリリースされたKotlinは、Scalaに影響を受けたが、実用性を考えてなのかゴリゴリの関数型プログラミングはやりたくなかったようで、case classをデチューンしてcopyメソッドが生え、equals, hashCode, toStringメソッドが自動的に生成されるdata classとして出した。
2018年にリリースされたPython 3.7は、それに影響を受けたのかわからないが、@dataclassという機能をリリース
2020年にデータと処理が一緒になっていた言語であるJavaが、Java14でrecordクラスというdata classのようなものをお試し版としてリリースして、2021年にJava16で正式版としてリリース。

代入って式じゃなくて文だよね。そのせいでループ終了判定がきれいに書けないんだよね

結論、再帰関数で書きましょう。Kotlinではtailrecという修飾子を使うことで末尾再帰最適化が使えます。これもScalaから受け継いだ機能です。
末尾再帰最適化とは、再帰関数の末尾で自分自身を呼び出す形になっているとき、コールスタックを使わずにループへ変換してくれるという最適化です。
ループ終了が、ループの都度の判定じゃないと分からないとき、再帰関数を使うときれいに書けます。
なぜ代入が文なのかというと、変数の値を変化させるという意味なので、代入が式ではなく文となっています。

// 末尾再帰最適化を使って、再帰によるループ
tailrec fun recursiveConsumer(queue: LinkedBlockingQueue<Int>) {
    val item = queue.take()
    if (item != -1) {
        println(item)
        recursiveConsumer(queue)
    }
}
recursiveConsumer(q)

// whileループ
fun consumerLoop(queue: LinkedBlockingQueue<Int>) {
    var i = 0
    while ((i = queue.take()) != -1) { // 代入が式ではないため、エラーとなる
        println(i)
    }
}
consumerLoop(q)

マスコットキャラクターとか居ないの?

居ます!Kodeeくんと言います!
参考記事:Kotlin マスコットの Kodee (コディー)をご紹介! | The Kotlin Blog

GitHubで編集を提案

Discussion