読者です 読者をやめる 読者になる 読者になる

vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

Kotlin Koansやってみたメモ Conventions編

Kotlin

はじめに

前回の続きです。

今回はConventionsにトライしたときのメモです。

ここではまずComparisonで下記のようなDataクラスを作り、これをベースに問題を解いていきます。

data class MyDate(val year: Int, val month: Int, val dayOfMonth: Int) : Comparable {
    override fun compareTo(other: MyDate) = when {
        year != other.year -> year - other.year
        month != other.month -> month - other.month
        else -> dayOfMonth - other.dayOfMonth
    }
}

このクラスではComparableを継承し、compareToで日付の比較ができるようにしています。

Conventions - In range

KotlinではOperatorを オーバーロードすることができます。

例えば下記のTestDataクラス同士は、「+」または「plus」で足し合わせることができます。

data class TestData(var num: Int, var text: String){
    operator fun plus(addData: TestData): TestData = TestData(this.num + addData.num, this.text + addData.text)
}
@Throws(IOException::class)
override fun start(primaryStage: Stage) {
    var originaldata = TestData(0, "test")
    var adddata = TestData(1, "2")

    // どちらもOK
    originaldata = originaldata.plus(adddata)
    originaldata = originaldata + adddata
}

で、ここではある日付(MyDate)が指定の期間内に含まれるかをチェックするため、containsを持つDataRangeクラスを作成します。

class DateRange(val start: MyDate, val endInclusive: MyDate){
    operator fun contains(item: MyDate) = item >= start && item <= endInclusive
}

Kotlin Koansではこのクラスを「in」を使って利用しています。

@Throws(IOException::class)
override fun start(primaryStage: Stage) {
    val dateTarget = MyDate(2016, 10, 9)
    val dateFirst = MyDate(2016, 9, 30)
    val dateLast = MyDate(2017, 11, 1)
    // Trueが返る
    val result = checkInRange(dateTarget, dateFirst, dateLast)
}
fun checkInRange(date: MyDate, first: MyDate, last: MyDate): Boolean {
    return date in DateRange(first, last)
}

他にも下記のように呼び出すことも可能です。

@Throws(IOException::class)
override fun start(primaryStage: Stage) {
    val dateTarget = MyDate(2016, 10, 9)
    val dateFirst = MyDate(2016, 9, 30)
    val dateLast = MyDate(2017, 11, 1)

    val dataRange = DateRange(dateFirst, dateLast)
    // Trueが返る
    var result = dataRange.contains(dateTarget)
}

なお、containsで日付を比較するところでは、先に実装しているcompareToが呼ばれます。

Conventions - Range to

MyDateクラスに日付の範囲を返すrangeToを追加します。

operator fun MyDate.rangeTo(other: MyDate) = DateRange(this, other)
class DateRange(override var start: MyDate, override val endInclusive: MyDate): ClosedRange

fun checkInRange(date: MyDate, first: MyDate, last: MyDate): Boolean {
    return date in first..last
}

ある日付(date)が日付の範囲(firstからlast)に含まれるかを確認している「date in first..last」は、下記のように書くことも可能です。

val dateTarget = MyDate(2016, 10, 9)
val dateFirst = MyDate(2016, 9, 30)
val dateLast = MyDate(2017, 11, 1)

// 日付の範囲(DataRange)を作る
val dateRange = dateFirst.rangeTo(dateLast)
// Trueが返る
val res = dateRange.contains(dateTarget)

ここで気になったのがDateRangeクラス。
引数がOverrideされていて、Kotlinてこんなこともできるのかぁ(こなみかん)という感じだったのですが、これはClosedRangeクラスを継承しているためのようです。

サブクラスで引数の値を変更するにはOverrideが必要、という。

Conventions - For loop

For-loop rangeについて。

ここではDateUtil.ktも使って解いていくのですが、面倒なので一つのクラスにまとめて書いていたらちょっとハマりましたorz

data class MyDate(val year: Int, val month: Int, val dayOfMonth: Int) : Comparable {
    override fun compareTo(otherItem: MyDate) = when {
        year != otherItem.year -> year - otherItem.year
        month != otherItem.month -> month - otherItem.month
        else -> dayOfMonth - otherItem.dayOfMonth
    }
}
operator fun MyDate.rangeTo(other: MyDate) = DateRange(this, other)

inner class DateRange(val start: MyDate, val end: MyDate): Iterable{
    override fun iterator(): Iterator = DateIterator(this)
}
fun MyDate.nextDay() = addTimeIntervals(TimeInterval.DAY, 1)

enum class TimeInterval {
    DAY,
    WEEK,
    YEAR
}
fun MyDate.addTimeIntervals(timeInterval: TimeInterval, number: Int): MyDate {
    val c = Calendar.getInstance()
    c.set(year + if (timeInterval == TimeInterval.YEAR) number else 0, month, dayOfMonth)
    var timeInMillis = c.getTimeInMillis()
    val millisecondsInADay = 24 * 60 * 60 * 1000L
    timeInMillis += number * when (timeInterval) {
        TimeInterval.DAY -> millisecondsInADay
        TimeInterval.WEEK -> 7 * millisecondsInADay
        TimeInterval.YEAR -> 0L
    }
    val result = Calendar.getInstance()
    result.setTimeInMillis(timeInMillis)
    return MyDate(result.get(Calendar.YEAR), result.get(Calendar.MONTH), result.get(Calendar.DATE))
}
inner class DateIterator(val dateRange:DateRange) : Iterator {
    var current: MyDate = dateRange.start
    override fun next(): MyDate {
        val result = current
        current = current.nextDay()
        return result
    }
    override fun hasNext(): Boolean = current <= dateRange.end
}
fun iterateOverDateRange(firstDate: MyDate, secondDate: MyDate, handler: (MyDate) -> Unit) {
    for (date in firstDate..secondDate) {
        handler(date)
    }
}
  • DateRange、DateIteratorクラスは「inner」をつけて内部クラスにする必要があり、ただネストしただけでは外側にある「MyDate.nextDay()」などにアクセスできません。

Iterable、Iterator

この問題では、for文である期間(firstDate)からある期間(SecondDate)までの日をhandlerに渡す、という処理を作ります。

で、まず「date in firstDate..secondDate」を実現するために「Iterable」を継承したクラス(DateRamge)が必要で、このクラスを作るために「Iterator」を継承したクラス(DateIterator)が必要になる、ということになります。

Iteratorでは渡された日付が「secondDate」より前かを確認する「hasNext」と、次の日を返す「next」を実装しています。

Conventions - Operators overloading

日付の足し算と、それを繰り返して処理するためのクラスを作ります。

で、そうそうにギブアップしましてorz、答えを見てみました。

data class MyDate(val year: Int, val month: Int, val dayOfMonth: Int)

enum class TimeInterval { DAY, WEEK, YEAR }

operator fun MyDate.plus(timeInterval: TimeInterval): MyDate = addTimeIntervals(timeInterval, 1)

fun task1(today: MyDate): MyDate {
    return today + TimeInterval.YEAR + TimeInterval.WEEK
}

class RepeatedTimeInterval(val timeInterval: TimeInterval, val number: Int)
operator fun TimeInterval.times(number: Int) = RepeatedTimeInterval(this, number)
operator fun MyDate.plus(timeIntervals: RepeatedTimeInterval) = addTimeIntervals(timeIntervals.timeInterval, timeIntervals.number)


fun task2(today: MyDate): MyDate {
    return today + TimeInterval.YEAR * 2 + TimeInterval.WEEK * 3 + TimeInterval.DAY * 5
}
fun MyDate.addTimeIntervals(timeInterval: TimeInterval, number: Int): MyDate {
    val c = Calendar.getInstance()
    c.set(year + if (timeInterval == TimeInterval.YEAR) number else 0, month, dayOfMonth)
    var timeInMillis = c.getTimeInMillis()
    val millisecondsInADay = 24 * 60 * 60 * 1000L
    timeInMillis += number * when (timeInterval) {
        TimeInterval.DAY -> millisecondsInADay
        TimeInterval.WEEK -> 7 * millisecondsInADay
        TimeInterval.YEAR -> 0L
    }
    val result = Calendar.getInstance()
    result.setTimeInMillis(timeInMillis)
    return MyDate(result.get(Calendar.YEAR), result.get(Calendar.MONTH), result.get(Calendar.DATE))
}

とりあえずtask1の方は問題なさそうですね。MyDateに対して「+」を使ったときに、関数オブジェクト「addTimeIntervals」が呼ばれるようにする、と。

ちなみに、関数オブジェクトの書き方が以前は「operator fun MyDate.plus(timeInterval: TimeInterval): MyDate = ::addTimeIntervals」のようになっていましたが、仕様変更があったのですね(今更感)。

問題はtask2の方。

MyDateに対して「* 数値」が行われた場合に、「+」をその数値文繰り返す、という内容となります。
で、この繰り返しを担う関数が「times」です。

「TimeInterval.times(number: Int)」で「RepeatedTimeInterval」というクラスが返されるようにしています。

で、このままだと「+」の型が違う(´;ω;`)とエラーになるので、「MyDate.plus(timeIntervals: RepeatedTimeInterval)」で対応、という流れです。

これ、今もなお答え見ないと回答できる気がしない...orz

Conventions - Destructuring declarations

Data クラスは分割して個別に扱うことができます、という内容でした。

data class MyDate(val year: Int, val month: Int, val dayOfMonth: Int)

@Throws(IOException::class)
override fun start(primaryStage: Stage) {
    val date = MyDate1(2016, 11, 12)
    val (year, month, dayOfMonth) = date

    println(year.toString())
    println(month.toString())
    println(dayOfMonth.toString())
}

こんな感じ。

Conventions - Invoke

最後はInvokeの問題。

class Invokable {
    public var numberOfInvocations: Int = 0
        private set
    operator public fun invoke(): Invokable {
        numberOfInvocations++
        return this
    }
}
fun invokeTwice(invokable: Invokable) = invokable()()

例えば下記のようにすると「invoke()」が2回呼ばれるので「numberOfInvocations」の値は2になります。

@Throws(IOException::class)
    override fun start(primaryStage: Stage) {
    // 初期化時はnumberOfInvocations = 0
    var invokable = Invokable()
    // invoke()が二回呼ばれるのでnumberOfInvocations = 2
    invokable()()
}

このInvokeですが、ググってみるとカリー化と呼ばれる複数の引数を持つ一つの関数を、一つの引数を持つ複数の関数に置き換える際に登場しているようです。

これについてはこの辺りが参考になりそうです。

おわりに

ConventionsではKotlin特有のテクニック、というよりはKotlinを使って「+」などのオペレーターを自分で作ることで言語仕様に触れる、という内容だった気がします。

正直まだ回答を見ながらふむふむそういう風に書くのか、と思っているだけの状態ではあるので、時間を置いてもう少し理解できるようになったら追記するか別に訂正記事を書こうかと思います。

本当は11/26のKansai.ktまでに、次のCollectionsまで行きたかったところですが、このペースだと資料作る時間もなくなりそうなので一旦ストップしてそちらの準備を進めたいと思います。

kansai-kt.connpass.com

参考

Operator overloading

ClosedRange

Iterator

分解宣言

カリー化