Kotlinの良いところ

adventar.org

Muroran Institute of Technology Advent Calendar 2017

Dec. 18

Kotlin のここが良い

Kotlinという名前

「Kotlinは可愛い.」

初めてその名を見た時,私はそれがプログラミング言語であるとは気付かなかった. Kotlinを書き始めて数週間,私は,Kotlinは実はキメラなのではないかと思い始めた. 現在,私はKotlinの魔力に囚われている.

実行または開発環境

  • JVM上で動く
  • 環境構築が楽
  • Javaのライブラリを利用できる
  • Androidアプリも開発できるらしい

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!")とシンプルです.

ここまでの内容をまとめると,

  1. Javaには記述の無駄が多い.
  2. Kotlinはそれらの無駄を削ぎ落としたものとなっている.

と言えるのではないでしょうか.

不変性のサポート
  • val
  • デフォルトの修飾子

不変クラスはクラスの理想の姿と言えます. スレッドセーフであり,意図しない変更を受けず,矛盾した状態となることもないからです. 例えば,不変クラスのインスタンスへの参照を保持する読み取り専用の変数は, いつでもどこでも初期化時と同じ値を保つため,取り扱いが非常に楽です. プログラムを書くときは,不変クラスを作成したり,変数を読み取り専用としたりことによって, できるだけ不変とすることが望ましいと思います.

しかし,Javaにおいて,これは生易しい話ではありません. 全ての変数にfinalを付けていく作業は心の折れる作業です. また,不変クラスを作成する際は,そのクラスのインスタンスが保持するオブジェクトが変更されないことを保証しなければいけません. これを実現するためには

  • 継承不可とする.
  • フィールドを読み取り専用とする.
  • 可変なオブジェクトを変更しない.
  • 可変なオブジェクトの参照を共有しない.

といったことに注意しなければいけません.

ここで,不変クラスは推奨されていますが,Javaでの実装が困難であるという問題があります. これに対して,Kotlinには不変性を意識したプログラミングを援護する文法があり, 不変クラスの実装を楽に行えます.

例えば,変数を宣言する時にval a = 1と書くと,変数aは読み取り専用となります. これはfinal int a = 1;をとするより,シンプルです.

また,クラスおよびメソッドはデフォルトでfinalが付いた状態となっております. 継承可能とするためまたはオーバーライド可能とするためにはopenを付けます. そして,C++とは逆に,アクセス指定はデフォルトでpublicとなります. はじめのうち,私は,カプセル化に反する感じがして慣れませんでした. しかし,よく考えると, フィールドはプロパティによってラップされているので, オブジェクトが不変である場合には困る事はありませんでした.

以上のように,Kotlinは言語として不変性をサポートしてくれます.

その他の便利機能
  • 演算子オーバーロード
  • 分解宣言
  • data class
  • 関数のデフォルト引数と名前付き引数
  • when式, if else式, throw catch式
  • Iterableの拡張関数

ここでは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

Javaソースコード

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の無駄を削ぎ落とし, 推奨される書き方をサポートし, 様々な言語の良い部分を寄せ集めた言語であると思います.

Javaを使っている人,特に,室蘭工業大学情報系のあなた,是非Kotlinを使ってみてください.