KotlinでFizzBuzzとは、Kotlinの記事内に書かれるべきサンプルコードであるが、あまりにも巨大なため分離することにした記事である。
KotlinでFizzBuzzを書くにあたって、Kotlinらしさを出すならDSL(Domain Specific Language)ではないかと考えた。FizzBuzzを書く際に必要な if と for をDSLに実装しつつ、言語の特長をコメントで紹介していたら、コードがあまりにも肥大化したので独立した記事にした。
無駄なことをしたと反省はしているが、以前から if と for はKotlinでは文法に規定しなくてもDSLの記法で書けるのではないかと疑問に思っていたので後悔はしていない。
普通のFizzBuzzはfizzBuzz1()に書いたので、ページ内を検索のこと。
package flowcontroldsl
/* "for" and "if" features implementation without using them
* to show the flexibility of Kotlin DSL(Domain Specific Language).
* Good boys and girls should not try.
*
* This contains some redundant codes to show Kotlin language features.
*/
/**
* エントリーポイントはトップレベル関数のmain().
* @param args Kotlin 1.3からは、この引数も省略可能になった
* @return [Unit]型(Javaのvoid相当)の戻り値型は省略可能
*/
fun main(args: Array<String>) {
fizzBuzz1()
fizzBuzz2(); fizzBuzz3() // ;は不要だが、使って1行に複数の文を書くことも可能
fizzBuzz4(41..60)
fizzBuzz5()
fizzBuzz6()
fizzBuzz7()
}
/**
* その気になればDSLを作ってifを自分で定義することも可能.
*
* 予約語も``を使えば識別子に使用可能。
* 本来はSystem.`out`などJavaで予約語が使われていた時用なので、
* 良い子はまねをしてはいけない。
* @param condition 条件
* @param trueOperation [condition]がtrueの時に実行されるラムダ式
* @param R if elseは式であり、値を返すが、その戻り値の型
* @return [condition]がfalseならnull。
* trueなら[trueOperation]の実行結果を[IfResult]でラップした値。
*
* @sample [fizzBuzz5]
*/
fun <R> `if`(
condition: Boolean, // 型名は後置
trueOperation: () -> R // 関数型の型名は(引数型) -> 戻り値型で記述
): IfResult<R>? { // ?はnullableの証
var ifResult: IfResult<R>? = null // varを使えば再代入可能
// ifの定義なのであえてifを避けた。大抵の言語にある論理演算の短絡評価を使用。
condition && { -> // ラムダ式は{}内に記述。引数なしなら->は省略可能。
ifResult = IfResult(trueOperation.invoke())
true // ラムダ式は最後の行が値を返す
}() // 関数型インスタンス(引数)で関数呼び出し。.invoke()相当。
return ifResult // 関数定義内の戻り値はreturnで返す
}
/**
* [R]がnullableの場合を考えると、conditionがfalseの場合を伝えるのに
* ラッパークラスが必要.
*
* クラスはコンストラクタやプロパティ(Javaのフィールド)と同時に定義可能。
* getterやsetterを書く必要ない。
* @property result ラップする値
* @see [else]
* @see [if]
*/
class IfResult<R>(val result: R)
/**
* クラス定義はデフォルトでpublic finalだが、
* 拡張関数により、クラス定義外でもインスタンスメソッドを追加定義できる.
* 本関数はクラス定義内でも書けるが、言語機能紹介のため。
*
* 拡張関数内ではクラス定義内部と同じように[this]が使える。
* この[this]が指すインスタンスのことをレシーバーと呼ぶ。
* レシーバーと呼ばれる由来がはっきりと書かれたところがないのだが、
* どうやら拡張関数の内部でメソッドの呼び出し(call)を
* 受ける(receive)インスタンスだからレシーバー(receiver)ということのようだ。
* 他のプログラミング言語でもこういう呼び方をすることがあるので、
* 一般的な用法だから説明不要ということなのかもしれない。
*
* また、今回は紹介できないがfinalなクラスでも継承するinterfaceがあれば
* 委譲(delegate)が使える。
* infixで中置記法も使用可能。
* @receiver [if]の結果
* @param falseOperation [this]がnullの時に呼ばれる処理
* @return if式の値
*/
infix fun <R> IfResult<R>?.`else`(falseOperation: () -> R): R {
// this?.resultはthisがnullの時resultは評価せずにnullを返す
// ?:はエルヴィス演算子と呼び、左項がnullでなければ左項の値、nullなら右項の値を返す
return this?.result ?: falseOperation()
}
/**
* else if 用 オーバーロード.
*
* 本物のif式との違いは[other]を生成するための条件式が必ず評価されてしまい、
* 短絡評価にならないこと。
* 通常、条件式に状態変更する内容が入ることはないが、
* 例外を投げるような式が入っていると問題になる可能性が残る。
* if ({条件のラムダ式}) {処理} とすれば解決できるが、見栄えの問題でやらない。
* @receiver この`else`の前までの[if]や`else` `if`の結果
* @param other この`else`の次の[if]の実行結果
* @return 次の[else]に渡す結果
*/
infix fun <R> IfResult<R>?.`else`(other: IfResult<R>?): IfResult<R>? {
return this ?: other
}
/**
* DSLの見栄えのために、クラス名の頭文字を大文字にするという原則を無視することは、
* Kotlin in Actionでも認められている。
*
* [For.break]と[For.continue]の不正な呼び出しを禁止するために、
* `for`を関数にしたり、Forをインターフェースにしたりする方法も試したが、
* 結局のところ、operationの内部でthisを外部のvarに代入することを
* 止められないので意味がないと判断した。
* @see [For]
*
* @sample [fizzBuzz6]
*/
typealias `for`<T> = For<T>
/**
* for文をDSLとして実装.
*
* continue, breakは例外処理で実現。ラベルのジャンプまで実装できた。
* @param T 繰り返し処理対象の型
*
* @sample [fizzBuzz7]
*
* @constructor
* Kotlinのfor文は for (e in iterable) {} の書式だが、
* ここでは`for` (iterable) { e -> } という記法になる.
* @param iterable 繰り返し処理の対象の[Iterable]
* @param operation 繰り返し処理の拡張関数ラムダ式。operation内部では、
* [Throwable]をcatchするtry式の中で[continue]や[break]を呼んではならない。
* [Exception]をcatchするtry式内で呼ぶのは問題ない。
*/
class For<T>(iterable: Iterable<T>, operation: For<T>.(T) -> Unit) {
/**
* delegated propertyで遅延評価するLazy<Nothing>に委譲.
* 遅延評価なので最初にプロパティにアクセスがあるまで例外は発生しない。
* @see [break]
*/
val `break`: Nothing by lazy { `break`() }
/**
* 必要ならgetter, setterをカスタマイズすることもできる.
* @see [continue]
*/
val `continue`: Nothing
get() {
`continue`()
}
/* ラムダ式がコンストラクタの引数と解釈されるので、For自身を
* 関数型オブジェクトにしてinvokeメソッドで起動する方法は無理で、
* operationはコンストラクタ内で起動するしかない。
*/
init {
/**
* あえて末尾再帰最適化tailrecを用いてループ構成する.
*
* [Iterable.forEach]を使えば簡単だが、
* [Iterable.forEach]のソースにforが使われているので。
*/
tailrec fun recursive(iterator: Iterator<T>) {
try {
operation(iterator.next())
} catch (e: ContinueException) {
`if` (e.destination !== this) {
throw e
}
}
// 通常のifでないとtailrecが効かないので
// `if`には中断する処理を入れた。
`if` (!iterator.hasNext()) {
this.`break`() // `if`内でreturnできないのでやむなく
}
return recursive(iterator)
}
try {
recursive(iterable.iterator())
} catch (e: BreakException) {
`if` (e.destination != this) { // 本当の参照等価は=== 上記参照
throw e
}
}
}
/**
* 本来は[Exception]を継承するべきところだが、それだと
* try {} catch (e: Exception) {}のような文脈で[break]や[continue]が
* throwする例外がcatchされて処理を抜けられない.
*
* try {} catch (e: Throwable) {} ではもちろんcatchされてしまうが、
* そもそも[Error]もcatchしてしまうようなコードは本来書くべきではないので、
* サポート対象外ということにする。
* @property destination ラベルの代わり
*/
private class BreakException(val destination: For<*>) : Throwable()
/**
* Kotlinでもinnerをつければインスタンスに内部クラスを定義することができるが、
* inner classで[Throwable]を継承するのはKotlin 1.3からエラーになる。
* 1.2でも思うように動作しなかった。
* インスタンスごとに例外を定義するのはNGで、innerをつけないで
* staticな内部クラスにしなければならないようだ。
*/
private class ContinueException(destination: For<*>) : Throwable() {
/**
* 必要ならプロパティ宣言はプライマリコンストラクタ宣言と分けられる.
*/
val destination: For<*>
get() = field // fieldは値の格納先を指す
init {
this.destination = destination
}
}
/**
* for文のbreak相当.
* [Throwable]をcatchするtry式の中で呼んではならない。
* [Exception]をcatchするtry式内で呼ぶのは問題ない。
*
* 当然だが、[For]のコンストラクタ外で呼んではならない。
*
* @param destination "this"か"this@ラベル名"のみ使用可能
* @throws BreakException 戻り値型が[Nothing] = 必ず例外を投げる
*
* @see [continue]
*/
fun `break`(destination: For<*> = this): Nothing { //デフォルト引数が使える
throw BreakException(destination)
}
/**
* Equivalent to "continue" in "for" statement.
* Do not call this method in try expression where [Throwable] is caught.
* You can call this method in try expression which catches [Exception].
*
* Of course, you should not call this method outside the [For] constructor.
*
* return@Forで代用できるが、[if]の中など代用できない場合もある。
* @param destination This argument should be "this" or "this@'label name'."
* @throws ContinueException 投げた例外は、[destination]の[for]でcatchされる
*/
fun `continue`(destination: For<*> = this): Nothing
= throw ContinueException(destination)
}
/**
* Kotlinらしさを主張した模範的FizzBuzz.
*/
fun fizzBuzz1() {
for (i in 1..20) { // ..演算子で生成するRangeは「閉」区間
when { // when式で複数分岐のif else地獄にさよなら
i % 15 == 0 -> "FizzBuzz" // case文のbreak忘れにもさよなら
i % 3 == 0 -> "Fizz"
i % 5 == 0 -> "Buzz"
else -> "$i" // string templateでフォーマット文字列も簡単
}.let(::println) // スコープ関数で変数定義を省けたりする
}
}
/**
* [fizzBuzz1]よりもちょっとハードコーディングっぽいFizzBuzz.
*
* Kotlinの言語機能紹介も兼ねているので無駄なこともしている。
*/
fun fizzBuzz2() {
for (i in 21 until 31) { // 左閉半開区間は..でなくuntil
val any = when (i % 15) { // 変数宣言はイミュータブルなvalが基本
in setOf(3, 6, 9, 12) -> "Fizz" // ListもSetもイミュータブル
in mutableListOf(5, 10) -> "Buzz" // 変更可能なのはMutableList
0 -> "FizzBuzz" as Any // キャスト
else -> i // 文字列にしないとwhen式はAny型を返していることになる。
}
print("""
$any
""".trimIndent()) // raw stringもstring templateと併用可能。
}
}
/**
* whenでなくif elseを使ったFizzBuzz.
*
* [println]の引数に式を書く方法ではif式の結果を2回使用する時には
* 変数宣言を省略できないが、
* [fizzBuzz1]でしたようなスコープ関数[let]を用いる方法なら可能である。
* ただし、[let]の呼び出しのほうがelse ifの結合より先なので、
* if式全体を括弧に入れる必要がある。
*/
fun fizzBuzz3() {
for (i in 31..40) { // Javaのfor-each文相当。Kotlinにfor(;;)はない。
println(if (i % 15 == 0) {
"FizzBuzz"
} else if (i % 3 == 0) {
"Fizz"
} else if (i % 5 == 0) {
"Buzz"
} else {
"${i}" // string templateは本来は{}で囲む
})
}
}
/**
* ワンライナー的FizzBuzz.
*
* [Sequence]にすると遅延評価になり[iterable]が巨大でもmap()が終わるまで待たずに済む。
* ラムダ式は変数名をitにすると変数名定義を省略可能。
*/
fun fizzBuzz4(iterable: Iterable<Int>) = iterable.asSequence().map {
"${if (it % 3 == 0) "Fizz" else ""}${if (it % 5 == 0) "Buzz" else ""}"
.ifEmpty { "$it" }
}.forEach(::println)
/**
* 関数の最後の引数がラムダ式の場合は括弧の外にラムダ式を書けるという仕様により、
* if式やfor文を自分で定義したDSL(Domain Specific Language)の記述が可能.
*/
fun fizzBuzz5() {
`for` (61..80) { i ->
// fizzBuzz2()のように型推論で変数宣言の型は省略可能
val string: String = `if` (i % 15 == 0) {
"FizzBuzz"
} `else` `if` (i % 3 == 0) {
"Fizz"
} `else` `if` (i % 5 == 0) {
"Buzz"
} `else` {
"$i"
}
println(string)
}
}
/**
* FizzBuzzから目的がずれてきているが、
* [for]の[For.continue]や[For.break]のデモンストレーション.
*/
fun fizzBuzz6() {
`for` (71..100) { i ->
// Exceptionインスタンスをcatchするtry式内でも動作することの実証
try {
`if` (i <= 80) { // 残念ながら中括弧は省略できない
`continue`
} `else` `if` (90 < i) {
`break`
}
} catch (e: Exception) {
println(e.message)
}
println(toFizzBuzz(i))
}
}
/**
* DSLで作った[for]でもlabelが使用できることのデモンストレーション.
*/
fun fizzBuzz7() {
`for` (9..12)label@ { i ->
`for` (i * 10 + 1 .. (i + 1) * 10) { j ->
println(toFizzBuzz(j))
`if` (j >= 100) {
//`continue`(this@label)
`break`(this@label)
}
}
}
}
/**
* 数値をFizzBuzzに変換.
*
* [fizzBuzz2]以上にハードコーディングっぽい。
* @param i Kotlinは表向きはプリミティブ型がないのでintでなくInt型
*/
fun toFizzBuzz(i: Int): String {
// Mapの生成は中置演算toを用いて下記のように書ける
val fizzBuzzMap = mapOf(
0 to "FizzBuzz", // ここの型はPair<Int, String>
3 to "Fizz",
6 to "Fizz",
9 to "Fizz",
12 to "Fizz",
5 to "Buzz",
10 to "Buzz"
)
// MapやListのget()は[]でできる
return fizzBuzzMap[i % 15] ?: i.toString()
}
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
掲示板
掲示板に書き込みがありません。
急上昇ワード改
最終更新:2024/04/19(金) 02:00
最終更新:2024/04/19(金) 02:00
ウォッチリストに追加しました!
すでにウォッチリストに
入っています。
追加に失敗しました。
ほめた!
ほめるを取消しました。
ほめるに失敗しました。
ほめるの取消しに失敗しました。