【Kotlin】リファクタリング：FizzBuzz

（2020-03-14 16:35 `asSequence()` を追加した。）

他の言語から移植してきたような FizzBuzz コードを、
Kotlin らしいコードにリファクタリングしていく。

---

初期状態。

```Kotlin
fun fizzBuzz0() {
    for (i in 1..100) {
        if (i % 3 == 0 && i % 5 == 0) println("FizzBuzz")
        else if (i % 3 == 0) println("Fizz")
        else if (i % 5 == 0) println("Buzz")
        else println(i)
    }
}
```

---

`if`-`else` を `when` に変えるとそれだけでかなりすっきりする。

```Kotlin
fun fizzBuzz1() {
    for (i in 1..100) {
        when {
            i % 3 == 0 && i % 5 == 0 -> println("FizzBuzz")
            i % 3 == 0 -> println("Fizz")
            i % 5 == 0 -> println("Buzz")
            else -> println(i)
        }
    }
}
```

---

`println` が何度も出てくるのが気になる。
`when` の返値としてまとめてから `println` するように変える。

```Kotlin
fun fizzBuzz2() {
    for (i in 1..100) {
        when {
            i % 3 == 0 && i % 5 == 0 -> "FizzBuzz"
            i % 3 == 0 -> "Fizz"
            i % 5 == 0 -> "Buzz"
            else -> i
        }.also {
            println(it)
        }
    }
}
```

`when` の返値を `val` 変数に代入して、それを `println` に渡してもよいが、
スコープ関数（`also` など）を使った方が
変数のスコープが狭くなるし、処理の流れが分かりやすい。

`also { println(it) }` は `also(::println)` と書いてもよいが、
前者の方が読みやすいと思う。
`also` はインライン関数なので実行効率に悪影響はない。

`also` の返値は使わないので、代わりに `let` を使っても動作は変わらない。
しかしこのような場合には `also` を使うようにしておいた方が比較的安全だ。
後でコードを書き換えて返値を使うことにしたときを考えよう。
`let` の返値、つまり `let` のブロックの返値を使うつもりであれば、
`let` のブロックの内容を意識しているはずだ。
実際には `also` が使われているのに `let` が使われていると勘違いして実装していても、すぐに気付けるだろう。
しかし逆に、実際には `let` が使われているのに `also` が使われていると思い込んでいると、
なぜ正しく動かない原因になかなか気付けないことがある。

---

`when` の返値は、
`i` が `3` か `5` の倍数の場合には `String` だが、
そうでない場合には `Int` である。
そのため、返値全体（`also` のブロック内での `it`）の型としては `Any` となっている。
`println` の引数型は `Any` であるため今は問題ないが、
美しくないし後で問題になるため `String` に統一しておく。

```Kotlin
fun fizzBuzz3() {
    for (i in 1..100) {
        when {
            i % 3 == 0 && i % 5 == 0 -> "FizzBuzz"
            i % 3 == 0 -> "Fizz"
            i % 5 == 0 -> "Buzz"
            else -> i.toString()
        }.also {
            println(it)
        }
    }
}
```

---

文字列のリストを作る工程と標準出力する工程とに分ける。
こうしておけば後で文字列のリストを再利用しやすい。

```Kotlin
fun fizzBuzz4() {
    (1..100).map { i ->
        when {
            i % 3 == 0 && i % 5 == 0 -> "FizzBuzz"
            i % 3 == 0 -> "Fizz"
            i % 5 == 0 -> "Buzz"
            else -> i.toString()
        }
    }.forEach {
        println(it)
    }
}
```

---

実は1つ前の変更にはデグレードがある。
変更前は1〜100が1〜100,000,000などになっても安定して出力を続けるが、
変更後は出力し始めるまでに時間が掛かり、場合によっては OutOfMemory になってしまう。
まず全ての数値を文字列に変換してリストを作り、それからそれを出力するからだ。
リストを作らず順次処理されるようにすることで改修する。

```Kotlin
fun fizzBuzz5() {
    (1..100)
        .asSequence()
        .map { i ->
            when {
                i % 3 == 0 && i % 5 == 0 -> "FizzBuzz"
                i % 3 == 0 -> "Fizz"
                i % 5 == 0 -> "Buzz"
                else -> i.toString()
            }
        }.forEach {
            println(it)
        }
}
```

`(1..100)` の次に `asSequence()` を追加したことで、`map` が `fun Iterable<T>.map(...): List<T>` ではなく `fun Sequence<T>.map(...): Sequence<T>` となった。

---

数の範囲（1〜100）、"Fizz"/"Buzz" への変換、標準出力への出力を
違うものに変更しやすいよう、
それらを引数で指定できるようにする。


```Kotlin
fun fizzBuzz6(
    numbers: Iterable<Int> = 1..100,
    toFizzBuzz: Int.() -> String = {
        when {
            this % 3 == 0 && this % 5 == 0 -> "FizzBuzz"
            this % 3 == 0 -> "Fizz"
            this % 5 == 0 -> "Buzz"
            else -> this.toString()
        }
    },
    consume: (String) -> Unit = { println(it) }
) {
    numbers
        .asSequence()
        .map { it.toFizzBuzz() }
        .forEach { consume(it) }
}
```

分かりにくくなってしまった。
ここまでやるかどうかは、再利用の可能性を考えて判断しよう。

おしまい。

## 補記:数値を文字列に変換する部分のリファクタリングと、FizzBuzz のルール

「3 で割り切れるなら Fizz、そうではなく 5 で割り切れるなら Buzz と出力する。
　ただし 3 でも 5 でも割り切れるならいずれでもなく FizzBuzz と出力する。」
というルールなら、数値を文字列に変換する部分は上記までの通り次のようなコードでよいだろう。

```Kotlin
    when {
        i % 3 == 0 && i % 5 == 0 -> "FizzBuzz"
        i % 3 == 0 -> "Fizz"
        i % 5 == 0 -> "Buzz"
        else -> i
    }
```

しかし「まず 3 で割り切れるなら Fizz と出力する。続いて 5 で割り切れるなら Buzz と出力する。（結果として、3 でも 5 でも割り切れるなら FizzBuzz と出力することになる。）」
というルールなら、次のようにする方が自然であり冗長さもなくなる。

```Kotlin
    buildString {
        if (i % 3 == 0) append("Fizz")
        if (i % 5 == 0) append("Buzz")
    }.takeIf { it.isNotEmpty() }
        ?: i.toString()
```