Takuji->find;

株式会社はてなでアプリケーションエンジニアやってます、技術的な記事を書いているつもり

Kotlinの演算子オーバーロードについて

これは Kotlin Advent Calendar 2016 9日目の記事です。

qiita.com

今日はKotlinの演算子オーバーロードについて紹介します。

演算子オーバーロードとは

演算を行うための記号が演算子です、だいたいのブログラミング言語にはありますよね?

+ - / * =&& || など、プログラミング言語にはたくさんの演算子があります。

Kotlinでは様々なクラスを演算子で演算した時の挙動をコントロールする演算子オーバーロードの機能があります。

これはJavaにはない機能ですね。

Kotlinの演算子オーバーロードの仕組み

Kotlinで演算子により演算を行う場合、左辺のクラスに存在する演算子に対応した名前で右辺の型にマッチするメソッドが存在した場合に行われます。

1 + 2 // 3

+plus メソッドを呼ぶので以下と同じ意味になります。

1.plus(2)

実際にKotlinで数値リテラルのメソッドを呼ぼうとすると、補完候補plus や他の演算子に対応したメソッドが出てきます。

ボウリングで学ぶ演算子オーバーロード

決まった名前でメソッドを作るだけです。これは通常のメソッド以外に拡張関数でも可能です。

私はボウリングが趣味なので、ボウリングのスコアを表現するクラスを作ります。

ボウリングは雑に説明すると、10本のピンに向かってボールを転がしていっぱい倒したらハッピーというスポーツです。

10本のピンは2回以内に倒しきらないといけなくて、1投目で倒しきったらストライク、2投目で倒しきったらスペア、倒しきれなかったらミス(エラー)となります。

ストライク、スペア、ミスのどれかで投げ終わる1つの流れをフレームといい、これを9回繰り返します。

9回繰り返した次の10フレーム目は最大3投でき、3投するかミスをした場合に終わります。

この10フレームが1ゲームになります。

クラスを定義

まずはスコアそのものを表現するクラスを作りましょう。

    object Strike : BaseScore(value = 10)
    object Spare : BaseScore(value = 0)
    class Score(value: Int) : BaseScore(value = value)
// ↓複雑になるので一旦は考慮しないことにします
//    object Foul : Score(value = 0)
//    object Miss : BaseScore(value = 0)
//    object Gutter : BaseScore(value = 0)
//    class Split(value: Int) : Score(value = value)
}

スペアは10-(1投目の投数)ですが、便宜的にオブジェクトで表現します。

次にフレームを作ります、9フレーム目までのフレームは1〜2つのスコアからなります

複雑度を下げるために、フレームは完了しないと計算できないようにします。

sealed class Frame(val scores: List<BaseScore>) {
    object StrikeFrame : Frame(listOf(Strike))
    class SpareFrame(firstScore: BaseScore) : Frame(listOf(firstScore, Spare))
    class MissFrame(firstScore: BaseScore, secondScore: BaseScore) : Frame(listOf(firstScore, secondScore))
}
class TenFrame private constructor(val scores:List<BaseScore>) {
    constructor(firstStrike: Strike, secondStrike: Strike, thirdScore: BaseScore) : this(listOf(firstStrike, secondStrike, thirdScore))
    constructor(strike: Strike, secondScore: Score, thirdScore: BaseScore) : this(listOf(strike, secondScore, thirdScore))
    constructor(firstScore: BaseScore, spare: Spare, thirdScore: BaseScore) : this(listOf(firstScore, spare, thirdScore))
    constructor(firstScore: BaseScore, missScore: Score) : this(listOf(firstScore, missScore))
}

10フレーム目だけ途中で投げ終わる可能性があるので、起こり得る全パターン(ただし3投目は何でもいいので固定)をコンストラクターで用意しました。

フレームが集まればゲームになります、ゲームを表現するクラスを作りましょう。

sealed class Game(val frames: List<Frame>) {
    class IncompletedGame(frames: List<Frame>) : Game(frames)
    class CompletedGame(frames: List<Frame>, tenFrame: TenFrame) : Game(frames)
}

本当は初期家事にフレーム数のバリデーションが必要ですが、今回は雑にこういう感じでいきます。

スコア + スコア = フレーム

さて、これを演算できるように実装していきましょう。

Score(1) + Score(8)  // ミス
Score(1) + Spare // スペア

まずは↑のパターンを作ります

open class Score(value: Int) : BaseScore(value = value) {
    open operator fun plus(b: BaseScore): MissFrame {
        return if (this.value + b.value < 10) {
            MissFrame(this, b)
        } else {
            error("Invalid score combination!")
        }
    }
    operator fun plus(spare: Spare): SpareFrame {
        return SpareFrame(this)
    }
}

この組み合わせで1つのフレームができあがります。

ストライク + スコア = フレーム2つ (完了していないゲーム)

ストライクだけは1つでフレームが終わってしまうので、ちょっと特殊です

Strike + Score(3) // ストライクと3ピン倒した状態、今回これは考慮しない
Strike + Strike // ダブル
Strike + (Score(3) + Spare) // ストライクとスペア

これは Strike オブジェクトに演算子オーバーロードをしましょう。

object Strike : BaseScore(value = 10) {
    operator fun plus(strike: Strike): IncompletedGame {
        return IncompletedGame(listOf(StrikeFrame, StrikeFrame))
    }
    operator fun plus(frame: Frame) : IncompletedGame {
        return IncompletedGame(listOf(StrikeFrame, frame))
    }
}

これだけで大丈夫です、Strikeは1つでフレームになるので、演算時にStrikeFrameに暗黙的に変換しているような扱いにしています。

フレーム + フレーム = 完了していないゲーム

フレームが2つ以上集まると完了していないゲームになりますね。ストライク単体もフレームと同じ扱いです。

sealed class Frame(val scores: List<BaseScore>) {
    operator fun plus(b: Frame): IncompletedGame {
        return IncompletedGame(listOf(this, b))
    }
    operator fun plus(strike: Strike): IncompletedGame {
        return IncompletedGame(listOf(this, StrikeFrame))
    }
}

単純ですね。

完了していないゲーム + フレーム = 完了していないゲーム

完了していないゲームにどんどんフレームを足していきます、最終的には9つまで許容します。

class IncompletedGame(frames: List<Frame>) : Game(frames) {
    operator fun plus(b: Frame): IncompletedGame {
        return if (this.frames.size == 9) {
            error("Next frame is TenFrame!")
        } else {
            IncompletedGame(frames + b)
        }
    }
    operator fun plus(strike: Strike): IncompletedGame {
        return this + StrikeFrame
    }
}

10個目を足そうとするとエラーになります。

完了していないゲーム + テンフレーム = 完了したゲーム

ここまでくるとほぼ完成ですね、10フレーム目を足しましょう。

class IncompletedGame(frames: List<Frame>) : Game(frames) {
    operator fun plus(b: TenFrame): CompletedGame {
        return if (this.frames.size != 9) {
            error("Next frame is not TenFrame!")
        } else {
            CompletedGame(frames, b)
        }
    }
}

使ってみる

ここまできたら、1ゲームを表現する演算ができるようになるはずです。

実際に先日投げたこの3ゲームをKotlinで表現してみましょう。

1ゲーム目

Strike + Strike + (Score(9) + Score(0)) + (Score(7) + Spare) + Strike + (Score(9) + Spare) + (Score(7) + Score(1)) + Strike + Strike + TenFrame(Strike, Score(8))

2ゲーム目

(Score(7) + Spare) + Strike + (Score(9) + Spare) + Strike + (Score(9) + Spare) + (Score(9) + Score(0)) + (Score(9) + Spare) + (Score(9) + Spare) + (Score(8) + Spare) + TenFrame(Score(9), Spare, Score(7))

3ゲーム目

Strike + (Score(9) + Spare) + Strike + (Score(7) + Score(0)) + (Score(9) + Score(0)) + (Score(8) + Score(0)) + Strike + Strike + (Score(7) + Score(1)) + TenFrame(Strike, Score(9), Spare)

全部エラーなしに書けました!

実際のところ、入力のバリデーションが必要だったりするので、これで完成ではないですが、演算子オーバーロードとしてはこれで完成かなと思います。

まとめ

いかがでしたでしょうか、当たり前ですが右辺の型で戻す型を変えたりなどできるので、結構柔軟な計算ができるようになります。

これ以外にも、中置関数を使うと組み込み演算子以外のカスタム演算子として使うことができるようになります。

詳しくは公式ドキュメントをご覧ください。

kotlinlang.org

最後に

計算まで終わらなかったのでそのうち実装して公開します。

以下が今回のコードの全容です。

sealed class BaseScore constructor(val value: Int) {
    object Strike : BaseScore(value = 10) {
        operator fun plus(strike: Strike): IncompletedGame {
            return IncompletedGame(listOf(StrikeFrame, StrikeFrame))
        }
        operator fun plus(frame: Frame) : IncompletedGame {
            return IncompletedGame(listOf(StrikeFrame, frame))
        }
    }

    object Spare : BaseScore(value = 0)

    class Score(value: Int) : BaseScore(value = value) {
        operator fun plus(b: Score): MissFrame {
            return if (this.value + b.value < 10) {
                MissFrame(this, b)
            } else {
                error("Invalid score combination!")
            }
        }

        operator fun plus(spare: Spare): SpareFrame {
            return SpareFrame(this)
        }
    }
//    object Foul : Score(value = 0)
//    object Miss : BaseScore(value = 0)
//    object Gutter : BaseScore(value = 0)
//    class Split(value: Int) : Score(value = value)
}

sealed class Frame(val scores: List<BaseScore>) {

    operator fun plus(b: Frame): IncompletedGame {
        return IncompletedGame(listOf(this, b))
    }

    operator fun plus(strike: Strike): IncompletedGame {
        return IncompletedGame(listOf(this, StrikeFrame))
    }

    object StrikeFrame : Frame(listOf(Strike))
    class SpareFrame(firstScore: BaseScore) : Frame(listOf(firstScore, Spare))
    class MissFrame(firstScore: BaseScore, secondScore: BaseScore) : Frame(listOf(firstScore, secondScore))
}
class TenFrame private constructor(val scores:List<BaseScore>) {
    constructor(firstStrike: Strike, secondStrike: Strike, thirdScore: BaseScore) : this(listOf(firstStrike, secondStrike, thirdScore))
    constructor(strike: Strike, secondScore: Score, thirdScore: BaseScore) : this(listOf(strike, secondScore, thirdScore))
    constructor(firstScore: BaseScore, spare: Spare, thirdScore: BaseScore) : this(listOf(firstScore, spare, thirdScore))
    constructor(firstScore: BaseScore, missScore: Score) : this(listOf(firstScore, missScore))
}

sealed class Game(val frames: List<Frame>) {
    class IncompletedGame(frames: List<Frame>) : Game(frames) {
        operator fun plus(b: Frame): IncompletedGame {
            return if (this.frames.size == 9) {
                error("Next frame is TenFrame!")
            } else {
                IncompletedGame(frames + b)
            }
        }
        operator fun plus(b: TenFrame): CompletedGame {
            return if (this.frames.size != 9) {
                error("Next frame is not TenFrame!")
            } else {
                CompletedGame(frames, b)
            }
        }
        operator fun plus(strike: Strike): IncompletedGame {
            return this + StrikeFrame
        }
    }
    class CompletedGame(frames: List<Frame>, val tenFrame: TenFrame) : Game(frames)
}

class Main {
    companion object {
        fun main(args: Array<String>) {
            Score(1) + Score(8)  // ミス
            Score(0) + Score(10) // スペア
            Score(2) + Score(8) // スペア

            Strike + Strike + (Score(9) + Score(0)) + (Score(7) + Spare) + Strike + (Score(9) + Spare) + (Score(7) + Score(1)) + Strike + Strike + TenFrame(Strike, Score(8))
            (Score(7) + Spare) + Strike + (Score(9) + Spare) + Strike + (Score(9) + Spare) + (Score(9) + Score(0)) + (Score(9) + Spare) + (Score(9) + Spare) + (Score(8) + Spare) + TenFrame(Score(9), Spare, Score(7))
            Strike + (Score(9) + Spare) + Strike + (Score(7) + Score(0)) + (Score(9) + Score(0)) + (Score(8) + Score(0)) + Strike + Strike + (Score(7) + Score(1)) + TenFrame(Strike, Score(9), Spare)
        }
    }
}