KotlinでFizzBuzz 単語

コトリンデフィズバズ

8.4千文字の記事
  • twitter
  • facebook
  • はてな
  • LINE

KotlinでFizzBuzzとは、Kotlinの記事内に書かれるべきサンプルコードであるが、あまりにも巨大なため分離することにした記事である。

概要

KotlinでFizzBuzzを書くにあたって、Kotlinらしさを出すならDSL(Domain Specific Language)ではないかと考えた。FizzBuzzを書く際に必要な if と for をDSLに実装しつつ、言の特長をコメント紹介していたら、コードがあまりにも肥大化したので独立した記事にした。

駄なことをしたと反省はしているが、以前から if と for はKotlinでは文法に規定しなくてもDSLの記法で書けるのではないかと疑問に思っていたので後悔はしていない。

普通FizzBuzzfizzBuzz1()に書いたので、ページ内を検索のこと。

コード

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](Javavoid相当)の戻り値省略
*/
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?.resultthisnullの時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

ウォッチリストに追加しました!

すでにウォッチリストに
入っています。

OK

追加に失敗しました。

OK

追加にはログインが必要です。

           

ほめた!

すでにほめています。

すでにほめています。

ほめるを取消しました。

OK

ほめるに失敗しました。

OK

ほめるの取消しに失敗しました。

OK

ほめるにはログインが必要です。

タグ編集にはログインが必要です。

タグ編集には利用規約の同意が必要です。

TOP