🔥

式しか使ってはいけないKotlin

2020/11/02に公開

この記事はQrunchのサービス終了に伴う移行のための投稿です。

Qiita→Qrunch移行のためのQiita版の自己転載です。

@.yaegaki さんの[C#]式しか使ってはいけないC#という投稿を見て、自分もKotlinでやってみたくなったのでやりました。

Kotlinのifとwhenは式で、スコープ関数等もあるのでその辺を多用すれば簡単な気がしますね。

レギュレーション

元記事のレギュレーションを可能な限りそのままKotlinに置き換えました

  • 文を使わない
  • メソッドは式形式のmain関数のみとする

2つ目について補足

fun main(): Unit = TODO()

このTODO()部分に実装を書きます。
main関数の返り値Unitは省略可能ですが、一部の解で明示的に書く必要があるため、統一するために全ての解で明示的に書くものとします。

実装課題

元記事と同じです。

シンプルにFizzBuzz問題を実装します。
1-100までのFizzBuzzを標準出力に出力します。

解答

コレクション操作関数編

fun main(): Unit = (1..100).map {
    when {
        it % 15 == 0 -> "FizzBuzz"
        it % 3 == 0 -> "Fizz"
        it % 5 == 0 -> "Buzz"
        else -> it.toString()
    }
}.forEach(::println)

ね?簡単でしょ?

詳細説明

まず最初の1..100ですが、コレはKotlinのrengeTo演算子です。メソッドのシグネチャは operator fun rangeTo(other: Int): IntRange です。IntRangeIterable<Int>の実装で、今回の場合は1から100までの値を出力するIteratorを生成します。
map{}はみんな大好きコレクション操作関数の fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R>です。Kotlinのwhenは式であり、また、引数を与えないことで単純なif-elseの置換として使えます。Javaのswitchとは違い、一番上から条件を評価して最初にマッチした式のみが使われるため、最初に% 15を評価→3と5を任意の順番で評価、とすることで網羅的に評価が可能です。
最後に、List<String>に対してforEachを使ってprintln関数で結果を出力します。
::printlnという記法はCallable Referenceと呼ばれるもので、関数に対する参照を取得する奴です。

再帰編

コレクション操作関数を使うとあまりにも簡単なので禁止します。
とはいえ、Kotlinのforは式ではないので、式のみで1から100までの整数を生成する方法はコレクションか再帰くらいしかありません。
ということで再帰を使います。

fun main(args: Array<String>): Unit = runCatching {
    if (args.isEmpty()) main(arrayOf("1")) else when(val i = args[0].toInt()) {
        !in 0..100 -> throw RuntimeException()
        in 0..100 step 15 -> i to "FizzBuzz"
        in 0..100 step 3 -> i to "Fizz"
        in 0..100 step 5 -> i to "Buzz"
        else -> i to i.toString()
    }.let { (i, str) ->
        println(str)
        main(arrayOf((i + 1).toString()))
    }
}.getOrDefault(Unit)

IntelliJなら全然読めるんですが、Qiitaでご覧の皆様はいかがでしょうか。
ハイライトが足らなくて読めねぇ!って人のために画像も貼っておきます。
image.png

Rainbow Bracketsプラグイン、良いですよね。

詳細説明

まず、前提条件として、式形式のmain関数においてはreturnを使うことができません。そのため、例外を投げて無理矢理中断しています。runCatchingというのは要するにtry{}catch{}をしてくれる関数です。多分ScalaのTryとかの劣化版です。
今更ですが、Kotlinのmain関数の引数の有無は任意です。今回は再帰のために引数をアリにしています。最初に呼び出す時は引数なしで呼び出し、再帰呼び出しでは引数アリで呼び出します。
Kotlin 1.3のwhenは引数内でwhenスコープの変数の宣言が可能なのでそれを用いることでいい感じにできます。Kotlinのwhenはパターンマッチングは無いのですが、inによるレンジチェックとisによる型チェックが可能です。ちなみにこのinoperator fun <T> Iterable<T>.contains(element: T): Booleanです。
今回のFizzBuzzは1から100までなので、最初にループ回数が範囲内かを確認し、範囲外なら例外をぶん投げます。サヨウナラ。
次に、Rangestepという関数で間隔を指定できます。コレとinを利用して倍数判定を行っています。
また、出力用の文字列だけでなく再帰呼び出し用のiを送り出す必要があるため、to関数を用いてPair<Int, String>whenの外に送出しています。Pairを筆頭とする、data class等のcomponentN()が実装されているクラスはラムダ式の引数や変数への代入時に分解することができます。Scalaのunapplyみたいなものです。
この分解された2つの引数を用いて文字の出力と再帰関数を呼び出し、最後に終了用の例外処理を行って完成です。

追記:例外未使用編

@sdkei さんのコメントを受けて、例外を使わないバージョンを作りました


fun main(args: Array<String>): Unit = if (args.isEmpty()) main(arrayOf("1"))
else when (val i = args[0].toInt()) {
    !in 0..100 -> null
    in 0..100 step 15 -> i to "FizzBuzz"
    in 0..100 step 3 -> i to "Fizz"
    in 0..100 step 5 -> i to "Buzz"
    else -> i to i.toString()
}?.let { (i, str) ->
    println(str)
    main(arrayOf((i + 1).toString()))
} ?: Unit

こういうnull-safetyを悪用?して便利にnullを異常値として使うコード、割と書きますよね。

最後に

やった内容は丸パクリですが、完成したコードの違いはKotlinとC#の言語機能・標準ライブラリの差異が出た感じですね。
徹夜テンションでやってるのでコードも解説も最適化の余地があると思います。コメント・編集リクエスト歓迎。
何か他の面白い方法のコメントも待ってます。
気が向けばコード生成編をやります。

Discussion