LoginSignup
2
0

More than 3 years have passed since last update.

Kotlin で FizzBuzz / Sequence を使う

Last updated at Posted at 2019-02-02

Kotlin らしい FizzBuzz の例。

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

解説

    (1..100)

2項演算子 ..rangeTo 関数に変換される
左項が Int 型であれば、rangeTo 関数の返値は IntRange 型。
したがって 1..100IntRange(1, 100) と等価。

IntRange クラスは Iterable インターフェイスを実装した IntProgression クラスを継承しており、指定された範囲の整数を1刻みで順に取得できる。
この場合だと 1, 2, 3, ... , 100

        .map {

Iterable 型の拡張関数 map は、 Iterable オブジェクトの各要素から変換された要素を持つ List オブジェクトを生成する。
(この例では 1, 2, 3, ..., 100 という数値を要素とする Iterable オブジェクト から、"1", "2", "Fizz", ..., "Buzz" という文字列を要素とする List を生成する。)
その変換処理は map の後ろのブロック { } 内に記述する。
このブロック内において要素はデフォルトでは暗黙の変数 it で表される。

            when {
                it % (3 * 5) == 0 -> "FizzBuzz"
                it % 3 == 0 -> "Fizz"
                it % 5 == 0 -> "Buzz"
                else -> it.toString()
            }

when を用いて、各要素をその値に応じた文字列に変換する。
上に書いたものが優先されるため、 要素の値が 15 の倍数の場合は "Fizz""Buzz" とはならずに、 "FizzBuzz" となる。
when からの返値を文字列型に統一するため、3 と 5 のいずれの倍数でもない場合は toString 関数で数値を文字列に変換する。

whenmap のブロックの最後の式なので、 when の返値が map のブロックの返値となる。

        .forEach {

List クラスも Iterable インターフェイスを実装しているため、
Iterable 型の拡張関数 forEach を用いて各要素に対して処理を行う。

map と同様、処理は forEach の後ろのブロック { } 内に記述する。
このブロック内において要素はデフォルトでは暗黙の変数 it で表される。

            println(it)

println 関数は与えられた引数を文字列に変換し、末尾に改行を付けて、標準出力に出力する。

実行結果(標準出力)

1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
17
Fizz
19
Buzz
Fizz
22
23
Fizz
Buzz
26
Fizz
28
29
FizzBuzz
31
32
Fizz
34
Buzz
Fizz
37
38
Fizz
Buzz
41
Fizz
43
44
FizzBuzz
46
47
Fizz
49
Buzz
Fizz
52
53
Fizz
Buzz
56
Fizz
58
59
FizzBuzz
61
62
Fizz
64
Buzz
Fizz
67
68
Fizz
Buzz
71
Fizz
73
74
FizzBuzz
76
77
Fizz
79
Buzz
Fizz
82
83
Fizz
Buzz
86
Fizz
88
89
FizzBuzz
91
92
Fizz
94
Buzz
Fizz
97
98
Fizz
Buzz

発展(Sequence を使う)

fun main() {
    (1..100)
        .asSequence() // この行を追加
        .map {
            when {
                it % (3 * 5) == 0 -> "FizzBuzz"
                it % 3 == 0 -> "Fizz"
                it % 5 == 0 -> "Buzz"
                else -> it.toString()
            }
        }
        .forEach {
            println(it)
        }
}

Iterable 型の拡張関数 mapList を生成する。
つまり、すべての要素のインスタンスを生成する。
今回のように List を生成するのが map 1回だけであればほぼ影響はないが、kotlin.collections パッケージにある拡張関数群 をチェインして処理をするような場合、関数呼び出しごとに List が生成されるので非常に効率が悪い。

そのような場合にはできるだけ早い段階で Iterable 型の拡張関数 asSequence を用いて Sequence 型に変換してやるとよい。
List クラスも Iterable インターフェイスを実装している。)

Sequence クラスは Iterator オブジェクトを返すメソッド iterator を持っており、要素が1つ要求されるごとに1つ取得して(生成して)返すため、効率が良い。

kotlin.sequences パッケージの拡張関数群 には mapforEach など、kotlin.collections パッケージの拡張関数群 と同様のものが多数用意されている。(すべてではないが。)
そのため多くの場合は単に asSequence 関数呼び出しを追加してやるだけで恩恵を受けられる。

注意

ただし注意すべきことがある。
それは次の2点である。

  • 要素を要求しない限り要素が生成されない
  • 要素を要求するたびに要素が生成される

例えば次のような処理を考える。

class MyClass {
    companion object {
        /** 生成したインスタンスの数。 */
        var count: Int = 0
            private set
    }

    init {
        ++count
    }

    fun doMyWork() {}
}

fun main() {
    (1..100)
        .asSequence()
        .map {
            MyClass()
        }
        .forEach {
            it.doMyWork()
        }
    println(MyClass.count) // > 100
}

これは期待通り 100 を出力する。

では main 関数を次のように変えるとどうだろうか。

fun main() {
    val sequence = (1..100)
        .asSequence()
        .map {
            MyClass()
        }
        // forEach がない。
    println(MyClass.count) // > 0
}

なんと、これだと出力値は 0 になる。
要素を要求する forEach がないため、map のブロックが一度も呼ばれないからだ。

では次は?

fun main() {
    val sequence = (1..100)
        .asSequence()
        .map {
            MyClass()
        }
    sequence.forEach { it.doMyWork() } // forEach が…
    sequence.forEach { it.doMyWork() } // 2回
    println(MyClass.count) // > 200
}

なんと! これは 200 を出力する。
forEach を2回呼び出したことで、各要素が2回ずつ要求されたからだ。

このような時は拡張関数 toList を使って List オブジェクトに変換してやると良い。
(この例の場合だと、そもそも asSequence を使って Sequence オブジェクトにする必要がなくなるのだが。)

fun main() {
    val list = (1..100)
        .asSequence()
        .map {
            MyClass()
        }
        .toList()
    list.forEach { it.doMyWork() }
    list.forEach { it.doMyWork() }
    println(MyClass.count) // > 100
}

このように、Sequence クラスは効率は良いのだが使い方を間違えるとバグを生んでしまう。
慣れるまでは、上の例のように、 asSequence 関数を使ったら必ず toList 関数で終える、とした方が良いかもしれない。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0