Kotlinの良いところ
Muroran Institute of Technology Advent Calendar 2017
Dec. 18
Kotlin のここが良い
Kotlinという名前
「Kotlinは可愛い.」
初めてその名を見た時,私はそれがプログラミング言語であるとは気付かなかった. Kotlinを書き始めて数週間,私は,Kotlinは実はキメラなのではないかと思い始めた. 現在,私はKotlinの魔力に囚われている.
実行または開発環境
KotlinはJetBrainsで開発されたJVM向けの言語です. IntelliJ IDEAをインストールするとKotlinをすぐに使うことができます. したがって,環境構築は非常に楽です.
また,Kotlinで書いたコードはコンパイルされた後,Javaと同様にJVM上で実行されます. さらに,JavaのライブラリをKotlinのプログラムから利用することもできます. Gradleのプラグインも存在しており,これを使用したMavenのライブラリの利用も可能です.
ちなみに,私は試していませんが,Androidアプリも開発できるようです.
文法に関して
Javaの無駄を排除
null
は無駄- セミコロンは無駄
new
は無駄- 変数宣言時の型名は無駄
- main関数にとってクラスは無駄
Javaによるプログラミングにおいて,null
は諸悪の根源であり,
これを駆逐するためにはnull
チェックを欠かしてはいけません.
しかし,これほど面倒臭いことがありましょうか?
そして,そもそもnull
は必要なのでしょうか?
Haskellを見れば,変数が無くても,プログラムを作成することが可能であると分かります.
読み取り専用の変数をnull
で初期化することに意味は無く,
したがって,読み取り専用の変数はnull
以外の値で初期化されます.
また,全ての変数が読み取り専用でもプログラムは正しく動くことが可能です.
すなわち,null
を用いずともプログラムは正しく動くのです.
null
は不要です.
それどころか,バグの根またはnull
チェックの手間以外の何物でも無いのです.
Kotlinにはnonnull型とnullable型があります.
nonnull型の変数にnull
を代入し,またはnull
で初期化しようとするとコンパイルエラーとなります.
このnull
安全な文法により,プログラマは憎っくきnull
に触ることなくコードを書くことができます.
このnull
安全性は私がScalaではなくKotlinを選ぶ決め手となりました.
1行に複数の文が存在する事は稀で,そもそも,そういう書き方は好まれません. 実際,Pythonは改行で文を区切ることとなっています. すなわち,文末のセミコロンは不要なのです.
Java, C++等では,当たり前のように何千何万何億ものセミコロンを書いてきましたが, Kotlinではその無駄な作業をせずに済むのです.
Javaにおいて,オブジェクト生成時にはコンストラクタの呼び出しおよびnew
を記述しなければいけません.
コンストラクタの呼び出しを見れば,生成するオブジェクトのクラスおよび呼び出すコンストラクタに渡す引数といった,
オブジェクト生成に必要な全ての情報が分かります.
では,new
を見るとどんな情報が得られるでしょうか,いいえ,どんな情報も得られません.
Javaにおいて,new
は不要なはずなのです.
Kotlinでは,この無駄も排除されるのです.
私はこの点においてKotlinはJavaおよびScalaより良いと思っています.
変数宣言時の型名を省略しても良い言語が多く存在します.
Kotlinもその一つです.
C++にもauto
ができましたね.
Javaにおいて,なぜ1つの変数宣言で同じ型名を2度も書かなければならないのかと思った事はありませんか?
明らかに無駄な型名を書かなくてはいけないというJavaの苦しみから,型推論があなたを解放します.
Hello World!を書くためだけになぜクラスを作らなければいけないのか,なぜ標準出力 (System.out.println("Hello World!");
)がめんどくさいのか?
KotlinはC++のように関数をクラス外に書けます.
ついでに言うと,標準出力もprintln("Hello World!")
とシンプルです.
ここまでの内容をまとめると,
- Javaには記述の無駄が多い.
- Kotlinはそれらの無駄を削ぎ落としたものとなっている.
と言えるのではないでしょうか.
不変性のサポート
- val
- デフォルトの修飾子
不変クラスはクラスの理想の姿と言えます. スレッドセーフであり,意図しない変更を受けず,矛盾した状態となることもないからです. 例えば,不変クラスのインスタンスへの参照を保持する読み取り専用の変数は, いつでもどこでも初期化時と同じ値を保つため,取り扱いが非常に楽です. プログラムを書くときは,不変クラスを作成したり,変数を読み取り専用としたりことによって, できるだけ不変とすることが望ましいと思います.
しかし,Javaにおいて,これは生易しい話ではありません.
全ての変数にfinal
を付けていく作業は心の折れる作業です.
また,不変クラスを作成する際は,そのクラスのインスタンスが保持するオブジェクトが変更されないことを保証しなければいけません.
これを実現するためには
- 継承不可とする.
- フィールドを読み取り専用とする.
- 可変なオブジェクトを変更しない.
- 可変なオブジェクトの参照を共有しない.
といったことに注意しなければいけません.
ここで,不変クラスは推奨されていますが,Javaでの実装が困難であるという問題があります. これに対して,Kotlinには不変性を意識したプログラミングを援護する文法があり, 不変クラスの実装を楽に行えます.
例えば,変数を宣言する時にval a = 1
と書くと,変数a
は読み取り専用となります.
これはfinal int a = 1;
をとするより,シンプルです.
また,クラスおよびメソッドはデフォルトでfinal
が付いた状態となっております.
継承可能とするためまたはオーバーライド可能とするためにはopen
を付けます.
そして,C++とは逆に,アクセス指定はデフォルトでpublic
となります.
はじめのうち,私は,カプセル化に反する感じがして慣れませんでした.
しかし,よく考えると,
フィールドはプロパティによってラップされているので,
オブジェクトが不変である場合には困る事はありませんでした.
以上のように,Kotlinは言語として不変性をサポートしてくれます.
その他の便利機能
ここではKotlinの文法に関するその他の良いところを挙げていきます. まず,演算子のオーバーロードです. やはり,ベクトル,点その他の数学的なクラスは数値型の演算子を同じように使いたくなります. そんなときに,演算子のオーバーロードができる言語はありがたいです.
KotlinのクラスはcomponentN()
を実装することで,
オブジェクトの値を分解して,複数の変数を同時に初期化することができます.
class Vector(val x: Double, val y: Double, val z: Double) { operator fun component1(): Double = x operator fun component2(): Double = y operator fun component3(): Double = z } fun main(vararg args: String) { val (a, b, c) = Vector(1.0, 2.0, 3.0) println("$a, $b, $c") //1.0, 2.0, 3.0 }
さらに,Kotlinにはdata class
というものがあり,
単に
data class Vector(val x: Double, val y: Double, val z: Double)
と書くだけで,Javaのよくある次のようなクラスに
public final class Vector { private final double x; private final double y; private final double z; public Vector(double x, double y, double z){ this.x = x; this.y = y; this.z = z; } public double getX() { return x; } public double getY() { return y; } public double getZ() { return z; } @Override public String toString() { return "Vector(x="x + ", y=" + y + ", z=" + z + ")"; } }
を実装したクラスに更に
operator fun component1(): Double = x
, operator fun component2(): Double = y
およびoperator fun component3(): Double = z
を実装したこととなります.
Javaにおいて,例えば,コンストラクタなどで設定すべき引数が多くなってくると, 引数の順番が分からない,デフォルトの設定がある,といった事態が起きます. そんなときは組み合わせ爆発によりオーバーロードに限りがありません. その結果として,Builderを作ることとなるのではないでしょうか? Kotlinでは,以下のように,関数のデフォルト引数および名前付き引数を利用することができます.
data class Vector(val x: Double = 0.0, val y: Double = 0.0, val z: Double = 0.0) fun main(vararg args: String) { println(Vector(1.0, 2.0, 3.0)) //Vector(1.0, 2.0, 3.0) println(Vector(1.0, 2.0)) //Vector(1.0, 2.0, 0.0) println(Vector()) //Vector(0.0, 0.0, 0.0) println(Vector(y = 2.0)) //Vector(0.0, 2.0, 0.0) println(Vector(1.0, z = 3.0)) //Vector(1.0, 0.0, 3.0) println(Vector(z = 3.0, y = 2.0)) //Vector(0.0, 2.0, 3.0) }
そして,Kotlinでは条件分岐のためのwhen
およびif else
ならびに例外処理のためのtry catch
が全て式となり値を持ちます.
これらを値を持つ式として扱えば条件漏れなどを無くせるのではないでしょうか?
他に,Kotlinには拡張関数という機能があり,これによってIterable
のメソッドが便利になっています.
以下にその例を挙げます.
filter
map
zip
take
drop
reduce
fold
sortedBy
zipWithNext
find
ここでは私が気に入っている機能について紹介させていただきました.
Bezier 曲線を描くならどっち?
KotlinとJavaで,どちらの方が楽にプログラムを書けそうか比べてみます. ここでは,Bezier曲線クラスの実装を例にします.
Bezier曲線とは
\(n\)次のBezier曲線 \( \boldsymbol{B} : [0, 1] \to \mathrm{E} \) はパラメータ\(t \in [0, 1]\)に対応する点\(\boldsymbol{B}(t) \in \mathrm{E}\)を返す関数で, \(n + 1\)個の制御点\(\boldsymbol{p}_{i} \ (0 \leq i \leq n)\)を用いて,
$$\boldsymbol{B}(t) = \boldsymbol{B}_{0}^{n}(t)$$
と評価します.ただし,
$$\boldsymbol{B}_{i}^{0}(t) = \boldsymbol{p}_{i} \ (0 \leq i \leq n)$$ $$\boldsymbol{B}_{i}^{j}(t) = (1-t) \boldsymbol{B}_{i}^{j-1}(t) + t \boldsymbol{B}_{i+1}^{j-1}(t) \ (1 \leq j \leq n, 0 \leq i \leq n - j)$$
です.
Intervalクラス
package jumpaku.kotlin data class Interval(val begin: Double, val end: Double){ operator fun contains(t: Double): Boolean = t in begin..end }
package jumpaku.java; public final class Interval { public Interval(final double begin, final double end) { this.begin = begin; this.end = end; } private final double begin; private final double end; public double getBegin() { return begin; } public double getEnd() { return end; } public boolean contains(final double t) { return begin <= t && t <= end; } @Override public String toString() { return String.format("jumpaku.kotlin.Interval(begin=%.1f, end=%.1f)", begin, end); } }
Pointクラス
package jumpaku.kotlin data class Point(val x: Double = 0.0, val y: Double = 0.0) { fun divide(t: Double, that: Point): Point { return Point(x.divide(t, that.x), y.divide(t, that.y)) } private fun Double.divide(t: Double, that: Double): Double { return (1-t)*this + t*that } }
package jumpaku.java; import java.util.Objects; public final class Point{ public Point(final double x, final double y) { this.x = x; this.y = y; } public static Point ofX(final double x) { return new Point(x, 0.0); } public static Point ofY(final double y) { return new Point(0.0, y); } public static Point origin() { return new Point(0.0, 0.0); } private final double x; private final double y; public double getX() { return x; } public double getY() { return y; } public Point divide(final double t, final Point that) { Objects.requireNonNull(that); return new Point(divide(t, x, that.x), divide(t, y, that.y)); } private double divide(final double t, final double d0, final double d1) { return (1-t)*d0 + t*d1; } @Override public String toString() { return String.format("jumpaku.kotlin.Point(x=%.1f, y=%.1f)", x, y); } }
BezierCurveクラス
package jumpaku.kotlin class BezierCurve(vararg controlPoints: Point){ val controlPoints: List<Point> init { require(controlPoints.isNotEmpty()) { "control point is empty" } this.controlPoints = listOf(*controlPoints) } val domain: Interval = Interval(0.0, 1.0) fun evaluate(t: Double): Point { require(t in domain) { "t($t) is out of domain($domain)" } return decasteljau(t, controlPoints) } private fun decasteljau(t: Double, controlPoints: List<Point>): Point { return when { controlPoints.size == 1 -> controlPoints.first() else -> decasteljau(t, controlPoints.zipWithNext { a, b -> a.divide(t, b) }) } } }
package jumpaku.java; import java.util.*; public final class BezierCurve { public BezierCurve(Point... controlPoints) { Objects.requireNonNull(controlPoints); if (controlPoints.length == 0) { throw new IllegalArgumentException("control point is empty"); } if (Arrays.stream(controlPoints).anyMatch(Objects::isNull)){ throw new IllegalArgumentException("control point contains null"); } this.controlPoints = Collections.unmodifiableList(Arrays.asList(controlPoints)); } private final List<Point> controlPoints; private final Interval domain = new Interval(0.0, 1.0); public List<Point> getControlPoints() { return new ArrayList<>(controlPoints); } public Interval getDomain() { return domain; } public Point evaluate(double t) { if (!domain.contains(t)) { throw new IllegalArgumentException("t($t) is out of domain($domain)"); } return decasteljau(t, controlPoints); } private Point decasteljau(final double t, final List<Point> points) { if (points.size() == 1) { return points.get(0); } else { ArrayList<Point> result = new ArrayList<>(); for (int i = 0; i < points.size() - 1; i++) { result.add(points.get(i).divide(t, points.get(i + 1))); } return decasteljau(t, result); } } }
動作実験
Kotlinのソースコード
package jumpaku.kotlin fun main(vararg args: String) { println("Jumpaku") val bezierCurve = BezierCurve( Point(-1.0, -1.0), Point(-1.0, 1.0), Point(1.0, -1.0), Point(1.0, 1.0)) (0..4).map { i -> i/4.0 } .map { bezierCurve.evaluate(it) } .map { (x, y) -> "$x, $y" } .forEach(::println) }
実行結果
-1.0, -1.0 -0.6875, -0.125 0.0, 0.0 0.6875, 0.125 1.0, 1.0
package jumpaku.java; import java.util.stream.IntStream; public class Main { public static void main(String... args) { System.out.println("Jumpaku"); BezierCurve bezierCurve = new BezierCurve( new Point(-1.0, -1.0), new Point(-1.0, 1.0), new Point(1.0, -1.0), new Point(1.0, 1.0)); IntStream.rangeClosed(0, 4) .mapToDouble(i -> i/4.0) .mapToObj(t -> bezierCurve.evaluate(t)) .map(p -> p.getX() + ", " + p.getY()) .forEach(System.out::println); } }
実行結果
-1.0, -1.0 -0.6875, -0.125 0.0, 0.0 0.6875, 0.125 1.0, 1.0
JavaとKotlinで同じ実行結果を出力するプログラムを書きましたが, Kotlinの方がシンプルで良いと思います. また,ソースコードはどちらも同じ設計に基づいていることも感じられると思います.
まとめ
Kotlinについて思ったことをひたすら書きました. 私は,KotlinはJavaと同じパラダイムの言語でありながら, Javaの無駄を削ぎ落とし, 推奨される書き方をサポートし, 様々な言語の良い部分を寄せ集めた言語であると思います.