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..100
は IntRange(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
関数で数値を文字列に変換する。
when
は map
のブロックの最後の式なので、 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
型の拡張関数 map
は List
を生成する。
つまり、すべての要素のインスタンスを生成する。
今回のように List
を生成するのが map
1回だけであればほぼ影響はないが、kotlin.collections パッケージにある拡張関数群 をチェインして処理をするような場合、関数呼び出しごとに List
が生成されるので非常に効率が悪い。
そのような場合にはできるだけ早い段階で Iterable
型の拡張関数 asSequence
を用いて Sequence
型に変換してやるとよい。
(List
クラスも Iterable
インターフェイスを実装している。)
Sequence
クラスは Iterator
オブジェクトを返すメソッド iterator
を持っており、要素が1つ要求されるごとに1つ取得して(生成して)返すため、効率が良い。
kotlin.sequences パッケージの拡張関数群 には map
や forEach
など、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
関数で終える、とした方が良いかもしれない。