日々精進

aikoと旅行とプログラミング

Kotlinスタートブックを読んでいくよ【1日目】

先日行われたGoogle I/OでKotlinが制式採用されることが発表になりましたね.

jp.techcrunch.com

ちょっときになったのでKotlinスタートブックを手に入れました. 最初から無理のないペースで読んでいき, メモ代わりにブログを書いていこうと思います.

第1章 Kotlinとは

kotlinlang.org

Kotlin(ことりん)とは, あのIntelliJ IDEAやWeb Stormなどで有名なJetBrainsが開発している言語です. いわゆるJVM言語であり, JavaとKotlinの相互運用性は100%だそうです. Javaよりも簡潔に記述することができ, 安全なコードを記述することができるのも特徴と言えるでしょう.

第2章 Kotlinを始める

最初のプログラム

こういうときのお決まり, Hello world!からKotlinの特徴を見てみる.

package sample

fun main(args: Array<String>){
    println("Hello, world!")
}
  • 関数がクラスに属していない
    Kotlinでは, 関数や変数が必ずしもクラスに属している必要はありません.
  • セミコロン不要
    セミコロンがあっても動作はします.
  • 型名は後ろに
    Array argsではなく, Array argsです.
  • 関数定義はfun

第3章 Kotlinガイドツアー

有理数クラスを作る過程を通してKotlinの雰囲気をつかんでいきます. 有理数クラスってなんか聞いたことあるなと思ったら, Scalaスケーラブルプログラミング(通称コップ本)のやつでした.

有理数クラスの定義

まずはRationalクラスを定義していきます.

class Rational(val numerator: Int, val denominator: Int)
  • クラス名の後ろに定義されているコンストラクタを, プライマリコンストラクタと呼ぶ.
  • 変数名の後ろに, valもしくはvarを書くことでプロパティになる.(プロパティとは, getter, setterとfieldが合わさったようなもの)
  • valはimmutable, varはmutableな変数である. 実際にインスタンス化して試してみる.
class Rational(val numerator: Int, val denominator: Int)    

fun main(args: Array<String>) {
    val half = Rational(1, 2)
    println(half.numerator)   // 1
    println(half.denominator) // 2
}

numeratorとdenominatorはvalで宣言されているため, 以下のようなコードは実行できません.

class Rational(val numerator: Int, val denominator: Int)    

fun main(args: Array<String>) {
    val half = Rational(1, 2)
    half.denominator = 2    // denominatorはimmutableなため
}
メソッドの定義

オブジェクトの文字列表現を返すtoStringは標準では「クラス名@ハッシュ値」であるため, これを意味のある文字列を返す関数に書き換える(Overrideする)

class Rational(val numerator: Int, val denominator: Int){
    override fun toString(): String = "${numerator}/${denominator}"
}  

fun main(args: Array<String>) {
    val half = Rational(1, 2)
    println(half)  // 1/2
}
  • メソッドシグネチャ = hogeというのは, メソッドの戻り値がhogeであることを示しています.
  • 文字列リテラル内の"${…}“という表現は, 波括弧内の式を評価し埋め込む機能(Stringテンプレート)です.
イニシャライザ

初期化ついでに何か行いたいときには, initブロックを活用します. 今回は分母が0の場合に例外をスローする処理を書いていきます.

class Rational(val numerator: Int, val denominator: Int){
    init {
        require(denominator != 0, {"denominator must not be null"})
    }   
    override fun toString(): String = "${numerator}/${denominator}"
}  

fun main(args: Array<String>) {
    val half = Rational(1, 2)
    println(half)
}
  • requireは, 第一引数がfalseのときに, IllegalArgumentExceptionがスルーされる関数. この関数で以下のようなコードを実行すると
class Rational(val numerator: Int, val denominator: Int){
    init {
        require(denominator != 0, {"denominator must not be null"})
    }   
    override fun toString(): String = "${numerator}/${denominator}"
}  

fun main(args: Array<String>) {
    val half = Rational(1, 0)
}

次のようにエラーとなります.

Exception in thread "main" java.lang.IllegalArgumentException: denominator must not be null
    at Rational.&amp;lt;init&amp;gt;(Simplest version.kt:8)
    at Simplest_versionKt.main(Simplest version.kt:14)
非公開プロパティとメソッド

続いて, 約分を実装していきます. 約分は分母と分子のgcdでそれぞれを割れば良いです.

class Rational(val n: Int, val d: Int)
    init {
        require(d != 0, {"denominator must not be null"})
    }   
    private val g = gcd(Math.abs(d), Math.abs(n))
    val numerator = n / g
    val denominator = d / g
    override fun toString(): String = "${numerator}/${denominator}"
    tailrec private fun gcd(a: Int, b: Int) = 
        if(b == 0) a
        else (b, a%b)
}  

fun main(args: Array<String>) {
    val half = Rational(55, 100) 
    println(half) // 11/20
}
  • tailrecは末尾再帰最適化処理

実行すると, 11/20と表示されます.

演算子オーバーロード

有理数同士で演算できたら嬉しいので, その機能を実現するメソッドplusを実装します.

class Rational(val n: Int, val d: Int)
    init {
        require(d != 0, {"denominator must not be null"})
    }   
    private val g = gcd(Math.abs(d), Math.abs(n))
    val numerator = n / g
    val denominator = d / g
    override fun toString(): String = "${numerator}/${denominator}"
    tailrec private fun gcd(a: Int, b: Int) = 
        if(b == 0) a
        else (b, a%b)

    fun plus(that: Rational): Rational =
        Rational(
            numerator * that.denominator + that.numerator * denominator,
            denominator * that.denominator
        )
}  

fun main(args: Array<String>) {
    val r1 = Rational(1, 4) 
    val r2 = Rational(1, 2)
    println(r1.plus(r2)) // 3/4
}
  • Rationalはimmutableなので, plusメソッドでは新たなRationalインスタンスを生成して返している点に注意.
    先程よりは便利になりましたが, r1 + r2のように書くことができたらなお便利になりそうです. そこで演算子オーバーロードが活躍します.
operator fun plus(that: Rational): Rational =
    Rational(
            numerator * that.denominator + that.numerator * denominator,
            denominator * that.denominator
        )

メソッドplusに修飾子operatorを付けることで, r1 + r2のような演算を実現することができます.

fun main(args: Array<String>) {
    val r1 = Rational(1, 4) 
    val r2 = Rational(1, 2)
    println(r1 + r2)) // => 3/4
}

オーバーロードに対応するメソッドシグネチャは予め決められている. (a-bはa.minus(b)とか)

メソッドのオーバーロード

メソッド名は同じであるが, 引数の型や数が異なるメソッドを複数定義することをオーバーロードと言います. 先ほどの計算では, Rational + Rationalでしたが, Rational + Int(たとえばRational(1, 2) + 1)を実現できたら嬉しそうです. そこで, 次のような関数を追加します.

operator fun plus(n: Int): Rational = 
    Rational(numerator + n * denominator, denominator)

これで, Rational(1, 2) + 1のような演算を実現することができます.

拡張関数

Rational + Intの演算は前の節で実現できたが, 1 + Rationalは実現できていない. しかし, レシーバーであるIntにはそのようなメソッドは実装されていないため実際はできません. そこで, 既存のクラスやインスタンスを拡張する拡張関数(extension function)を定義してあげてこの機能を実現します.

operator fun Int.plus(r: Rational): Rational = r + this

拡張関数を使うとクラスを継承することなく機能を追加できるというわけですね.  

最終的なコード

class Rational(n: Int, d: Int){
    init {
        require(d != 0, {"denominator must not be null"})
    }   
    private val g = gcd(Math.abs(d), Math.abs(n))
    val numerator = n / g
    val denominator = d / g
    override fun toString(): String = "${numerator}/${denominator}"
    tailrec private fun gcd(a: Int, b: Int): Int = 
        if(b == 0) a
        else gcd(b, a % b)

    operator fun plus(n: Int): Rational = 
        Rational(numerator + n * denominator, denominator)

    operator fun plus(that: Rational): Rational =
        Rational(
            numerator * that.denominator + that.numerator * denominator,
            denominator * that.denominator
        )
}  

operator fun Int.plus(r: Rational): Rational = r + this

fun main(args: Array<String>) {
    val r1 = Rational(1, 4) 
    val r2 = Rational(1, 2)
    println(r1 + r2) 
    println(r1 + 1)
    println(1 + r1)
}

本の方だと, numeratorやdenominator, gがlazyになっていた.

private val g by lazy { gcd(Math.abs(d), Math.abs(n)) }
val numerator: Int by lazy{n / g} 
val denominator: Int by lazy{d / g}

lazyはlambdaをとるっぽい.
今日はここまで(P38まで読んだ)