FizzBuzz
Kotlin
Sequence

Kotlin で FizzBuzz / Sequence を使う

Kotlin らしい FizzBuzz の例。

fun main() {

(1..100)
.map {
when {
it % (3 * 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 関数で終える、とした方が良いかもしれない。