Kengo's blog

Technical articles about original projects, JVM, Static Analysis and TypeScript.

「誰が書いたコードか当てまShow!!」出題者による解説

勤務先のブログで紹介されていますが、Kotlin Fest 2025で出題したクイズに向けてKotlinコードを書いていました。本記事では自分で自分のコードを解説してみます。企画のネタバレを含みますので、先に企画紹介記事を読んでいただければ幸いです。

ヒントは「メソッドチェーン」「Kotlinのプロ、じゃない」

まず自分のコードを再掲します。パッと見でメソッドチェーン風だということがわかり、リアクティブプログラミングの経験があるひとにはAっぽいなと伝わる雰囲気を醸し出しています。

fun solve(input: String): String {
    var prevScore: Int? = null
    var prevRank = 1
    return input.split("\n")
        .map { it.split(" ") }
        .groupBy({ it[0] }, { it[1].toInt() })
        .mapValues { it.value.sum() }
        .toList()
        .sortedWith(compareByDescending<Pair<String, Int>> { it.second }.thenBy { it.first })
        .mapIndexed { index, (name, score) ->
            val rank = if (prevScore == null || prevScore > score) {
                index + 1
            } else {
                prevRank
            }

            (rank to "$name $score").also {
                prevScore = score
                prevRank = rank
            }
        }.takeWhile { it.first <= 3 }
        .joinToString("\n") {
            "${it.first}. ${it.second}"
        }
}

ひとつずつ見ていきましょう。まず目につくのが変数名です:

fun solve(input: String): String {
    var prevScore: Int? = null
    var prevRank = 1

prev は previous の短縮形です。このくらいなら短縮せずに previousScore としたほうが良いかもしれませんね。このくらいの変数なら特にコメントで意図を説明する必要はないかなと思って、ドキュメンテーションコメントは何も書いていませんでした。

なおBのコードでは previous ではなく last が使われており、どちらが良い命名なのかを議論するのも面白そうです。

さて次の行が大きなヒントになっています:

    return input.split("\n")

これとてもJavaっぽい書き方で、実際他の方の回答だと lineSequence() が使われているんですよね。ここで少なくとも「AはKotlinのプロであるタケハタのコードじゃないな」とバレるようになっています。なぜ lineSequence() を使わなかったのかって?だって、知 ら な い そ ん な 函 数!勉強になりました。

なおここで「OSがCRLFのような他の改行コードで動いてたらどうするんだ」ってツッコミは可能なんですが、この手の出題でそこが問題になることはないだろうと判断しました。厳密には System.lineSeparator() あたりを参照したほうが良いですね。

ちなみにJavaで似たような問題を解くと必ずと言っていいほど Scanner が出てくるんですが、さすがKotlin、流れるように処理を書き下せました。便利。

さて次のコードです:

        .map { it.split(" ") }
        .groupBy({ it[0] }, { it[1].toInt() })
        .mapValues { it.value.sum() }
        .toList()
        .sortedWith(compareByDescending<Pair<String, Int>> { it.second }.thenBy { it.first })

ここはまぁ、普通ですね。 Pair を引き回すと可読性が落ちるので、Dのようにクラスを定義して使うのも良さそうですが、今回はまぁいいでしょという気持ちになりました。今見返すと、 .groupBy({ it[0] }, { it[1].toInt() }) がだいぶ厳しいか。

あと compareByDescending() については、3分くらい「GuavaならOrderingなんだけどな〜」って検索してました。無事に便利函数が見つかって良かったです。

さて次は誰からも突っ込まれなかったコードです:

            (rank to "$name $score").also {
                prevScore = score
                prevRank = rank
            }

この also の使い方、だいぶ独特な感じに仕上げたつもりなんですが、見事にスルーされました。クリーンアップ処理を他のコードと違う場所にまとめて書ける、かつ finally と比較したときに例外の発生を想定してないことを伝えられる、ということでわりと悪くない書き方だと思うんですけどね。

最後はこのコードです:

        }.takeWhile { it.first <= 3 }
        .joinToString("\n") {
            "${it.first}. ${it.second}"
        }
}

お題を見たときに takeWhile() が出てくることは決めていました。一番最初に書けたコードだとも言えますね。あとは特に変わったところはなく、普通に joinToString() して終わりです。

他の人のコードへのツッコミどころ

ということで、以上で出題者による解説でした。楽しんでいただけたでしょうか。

なお他の人のコードにもツッコミどころというか、個性を感じるところは多々あります。ぜひ皆さんも考えてみてください。私が思ったのは次のような感じでしたが、人によって意見は異なると思いますので、ご参考まで:

  • 名前つき引数、いい。わかりやすい。IDEAで書いてるとなかなか気付けない気配りポイント。印刷前提だったので、自分も書いたほうが良かった。
  • String.split(String) は正規表現のコンパイルが走る可能性があるので、ループ内では呼ばないクセをつけたほうが良いかも
  • return@foobar は可読性が落ちるので、個人的には避けたい
  • tailrec すごくいい
  • コメントがいちいち意味ない!(たぶんツッコミ待ちなんだけど気になる)
  • is-a と has-a は違うんですよ!(たぶんツッコミ待ちなんだけど気になる)
  • 函数の引数の型はできるだけ抽象に寄せたいなぁ、なんで HashMap なんだっけ……(たぶんツッコミ待ちなんだけど気になる)