PR

FizzBuzzしてみよう

2018年8月16日(木)
伊藤 竜一
【Scalaはこわくない!マルチパラダイム言語Scalaの入門書!】株式会社インプレスR&Dより発行された「Scalaをはじめよう! ─マルチパラダイム言語への招待─」の立ち読みコーナー第4回です。

FizzBuzzしてみよう

for式・if式といった基本的な制御構文

 ここで扱うのは,みんな大好き“FizzBuzz”です.そう,「1からnまでの値を取り,3の倍数のときは Fizz を,5の倍数のときは Buzz を,それ以外のときは数値をそのまま出力する」というよくある問題です.書き方は色々有りますが,例えばJavaで書くとこんな感じでしょうか.

リスト4.1: JavaでのFizzBuzz

void fizzBuzz(int n) {
  for (int i = 1; i <= n; i++) {
    if (i % 15 == 0) {
      System.out.println("FizzBuzz");
    } else if (i % 3 == 0) {
      System.out.println("Fizz");
    } else if (i % 5 == 0) {
      System.out.println("Buzz");
    } else {
      System.out.println(i);
    }
  }
}

 これをScalaに置き換えていきましょう.

FizzBuzzに必要な道具を揃える① ─for式

 とりあえずループと条件分岐が必要ですね.Scalaにはループに使うことができる構文としてwhile式とfor式がありますが,ここではwhile式のことは一旦忘れて,for式を使って進めていきたいと思います.

JavaやCでよく行われるwhile文 + breakcontinueをScalaで実現することも可能ですがあまり推奨されていません.より賢く読みやすい方法で記述できる場合がほとんどですのでそちらを模索しましょう.

 JavaやCのfor文と似ていますが,Scalaではになっています.つまり値を返すことができます.また,“ジェネレータ” という構文を使うことで,for(int i = 0; i < 10; i++)のようにループカウンタを宣言して明示的にインクリメントしていくようなことはせず,JavaやC++で言う拡張for文(≒foreach文 ≒for-in文)に近い挙動をします.では1からnまでカウントアップしながら標準出力を行う簡単なサンプルを見てみましょう.

リスト4.2: for式によるループ

 1: val n = 3
 2: for { i <- (1 to n) }{
 3:   println(i)
 4: }
 5: /* 出力
 6: 1
 7: 2
 8: 3
 9: */

重 要なのはi <- (1 to n)の部分です.これを“ジェネレータ”と呼びます.<-はジェネレータを表す専用のキーワードとなっており,右辺がデータ源左辺が右辺から順番に取り出された値になります.先ほどのサンプルで見ると,右辺の(1 to n)1からnの値を,つまり1,2,3という値を生成し,左辺のi123を順番に利用できます.結果的にこのサンプルでは123が改行区切りで出力されるはずです.JavaやCでfor(int i = 1; i <= 3; i++)としたときと同じですね1

 ループの範囲を変えるにはジェネレータの右辺を変えればいいことがわかりました.次はもう少し高度なループを見ていきたいと思います.ターゲットにするJavaのサンプルは以下の通りです.

リスト4.3: Javaでのネストしたfor文

for (int i = 0; i <= 12; i += 3) {
  if (i % 2 == 0) {
    for (int j = 1; j <= 3; j++) {
      System.out.println(i * j);
    }
  }
}

 2重ループになっており,i3ずつインクリメントされたりifが挟まっていたりとやや複雑になっています.Scalaでは次のようになります.

リスト4.4: ネストしたfor式

 1: for {
 2:   i <- (0 to 12 by 3) if i % 2 == 0
 3:   j <- (1 to 3)
 4: } {
 5:   println(i * j)
 6: }

 「3ずつインクリメント」という文脈はby 3を追加するだけで実現できます.例えばby 2に変えると2ずつインクリメントされるようになります.次に見るべきはその後ろに付いているifです.これは生成される値から取り出すときの条件を指定できます.この例では偶数だけがiに渡されることになります.

 ただし,このifはあくまで生成される値から取り出すときの条件であり,値を生成するための条件ではありません.つまり,一旦0,3,6,9,12という値が作られてからi0612だけが渡されます.

 次は2つ目のjに関するジェネレータに注目しましょう.Scalaのfor式ではこのようにジェネレータを改行して複数並べることで多重ループを実現できます.今回は2つなので2重ループ相当ですが,何個でも並べることができます.先にあるジェネレータが外側のループに相当します.見た目の順序はJavaやCでfor文をネストさせた場合と同じですね.

 ここまで見てきたfor式をまとめると,以下のようになります.

リスト4.5: for式の概形

for {
  順番に取り出される値のための変数 <- データ源
  ...
} 式

 また現れました,for式の本体部分(後半部分)も式ですね.ジェネレータで定義した変数を利用してなんらかの計算を行うことになります2

 さて,ここで1つ気になる点はないでしょうか?forなのに返り値がないように見えませんか?実は本体の式にどんな式を入れようともUnit型の()を返してきます.

リスト4.6: for式で期待した返り値が得られない例

 1: val result = for {
 2:   i <- (1 to 3)
 3: } {
 4:   i
 5: } // >> ()
 6: // 単純に i を返すだけの式にしたい
 7: // つまり,`1`,`2`,`3` と3つの値が返ってきて欲しい
 8: // が,`result` は `Unit` 型の `()` になってしまう

 以前解説したようにUnit型は返り値がないことを意味するものです.困りましたね,このままではfor式が式である意味が感じられません.そこで登場するのがyieldというキーワードです.C#やPythonといった一部の言語では,Scalaのyieldとほぼ同じ意味のキーワードとして使われています.このキーワードをfor式に挿入することで,本体の式の値を返すことができるようになります.

リスト4.7: yieldを利用してfor式の返り値を得る

 1: val result = for {
 2:   i <- (1 to 3)
 3: } yield {
 4:   i
 5: } // >> `1`,`2`,`3` の3つの値からなるコレクション

 ここではresultVectorというコレクションになっていますが詳細は6章で説明します.現時点では配列のように順序ある値の集まりと考えてください.これを一般的な形で見てみると以下のようになります.

リスト4.8: yieldを利用した返り値のあるfor式の概形

for {
  順番に取り出される値のための変数 <- データ源
  ...
} yield 式

 本当にただyieldというキーワードが増えただけです.単純ですね.

FizzBuzzに必要な道具を揃える② ─if式

 さて次は条件分岐を見ていきましょう.まずはif式です.これもになっており,JavaやCの3項演算子に近い,より汎用的な挙動をします.一般的な形にすると以下のようになります.

リスト4.9: if式の概形

if (`Boolean` 型の値を返す式) 式 else 式

 条件式に応じてどちらかの式の結果がif式の結果として返されます.もちろんelse ifを利用した複数条件も記述できます.

さて,手札が揃ったところでScalaでFizzBuzzを書いていきましょう.

リスト4.10: for式とif式を利用したFizzBuzz

 1: def fizzBuzz(n: Int): Unit = for { i <- 1 to n } {
 2:   if (i % 15 == 0) {
 3:     println("FizzBuzz")
 4:   } else if (i % 3 == 0) {
 5:     println("Fizz")
 6:   } else if (i % 5 == 0) {
 7:     println("Buzz")
 8:   } else {
 9:     println(i)
10:   }
11: }

 概形は先に示したJavaのものと同じですね.

FizzBuzzの趣向を変えてみる① ─match式

 少し違う形でも書いてみましょう.match 式という構文を使ってみたいと思います.JavaやCで言うswitch文のお化けのようなものです.先ほどのFizzBuzzを書き直すと以下のようになります.

リスト4.11: for式とmatch式を利用したFizzBuzz

 1: def fizzBuzz(n: Int): Unit = for { i <- 1 to n } {
 2:   i match {
 3:     case x if x % 15 == 0 =>
 4:       println("FizzBuzz")
 5:     case x if x % 3 == 0 =>
 6:       println("Fizz")
 7:     case x if x % 5 == 0 =>
 8:       println("Buzz")
 9:     case x =>
10:       println(x)
11:   }
12: }

 一般的なswitch文と形はほぼ同じですが,より多くの条件を記述できるようになっています.上記の例ではx if x % 3 == 0xといった条件が使われています.caseの直後に現れるxは条件判定と判定後の処理のみで利用できる変数の宣言です3.この変数宣言のみの場合は全ての条件にマッチしますが,更に条件を加えることができます.x if x % 3 == 0は「x3で割り切れる場合に限定する」という条件,xは「全てにマッチする」という条件になります.列挙された条件は上から順番に評価され,最初にマッチした条件のみがその後の式の評価に進みます(JavaやCのような条件ごとのbreakはなく,複数の条件にマッチすることもありません).「15で割り切れる」場合はprintln("FizzBuzz")のみが実行され,そうでなく「3で割り切れる」場合はprintln("Fizz")のみが実行され,「5で割り切れる」場合はprintln("Buzz")のみが実行されます.そして,残りの全ての場合はprintln(x)のみが実行されるという寸法です.

 match式の一般的な形も見てみましょう.

リスト4.12: match式の概形

対象の式 match {
  case 1つ目の条件 => 式
  case 2つ目の条件 => 式
  ...
}

 対象の式(変数単体も式であることを忘れないでください)が評価された結果に基づいてパターンマッチが行われます.多くの場合,必要なパターンを上から順に記述し,最後にcase x => 式で残り全ての条件をキャッチすると良いでしょう4

 また,matchであるため,値を返します.

リスト4.13: match式の活用

 1: val data = 10
 2: val result = data match {
 3:   // 定数による条件
 4:   case 0 =>
 5:     "0です"
 6: 
 7:   // `|` によるOR条件
 8:   case 1 | 2 =>
 9:     "1か2です"
10: 
11:   case x if x % 3 == 0 =>
12:     // `toString` を呼び出すことで文字列に変換できます
13:     "0でも1でも2でもなく3で割り切れる値である" + x.toString + "です"
14: 
15:   case x =>
16:     // 文字列補完
17:     s"0でも1でも2でもなく3で割り切れない値である${x}です"
18: }
19: 
20: println(result)
21: /* 出力
22: "0でも1でも2でもなく3で割り切れない値である10です"
23: */

文字列補完

 複数の文字列と変数を組み合わせて文字列を作成するための文字列補完と呼ばれる機能があります.文字列を括るクォーテーション(")の前にsを付けることで,文字列内で${式}として変数を参照して式を記述できるようになります.なお,文字列補完やprintlnString型でない値が渡された時は暗黙的にtoStringが呼び出されます.

 定数条件の場合は定数をそのまま条件として利用します.この際|で区切ることでOR条件にできます.match式はここまで扱ってきた以外にも様々な条件に対応しています.本書でももう少し出てきます.

FizzBuzzの趣向を変えてみる② ─再帰

 ここで趣向を変えてループを使わずにFizzBuzzを書き直してみましょう.ループの代わりといえば再帰ですね.

リスト4.14: 再帰とmatch式によるFizzBuzz

 1: def fizzBuzz(n: Int,i: Int = 1): Unit = {
 2:   // 値に応じて出力
 3:   i match {
 4:     case x if x % 15 == 0 =>
 5:       println("FizzBuzz")
 6:     case x if x % 3 == 0 =>
 7:       println("Fizz")
 8:     case x if x % 5 == 0 =>
 9:       println("Buzz")
10:     case x =>
11:       println(x)
12:   }
13: 
14:   // `i` が `n` になるまで再帰呼び出し
15:   if (i < n) fizzBuzz(n,i + 1)
16: }
17: 
18: fizzBuzz(15) // 最初の呼び出し

 iをカウンタとして,nまで再帰呼び出しを行います.最初の呼び出しではi: Int = 1のデフォルト引数を利用しています(再帰呼び出しの際はi + 1を利用).

 Scalaは関数型プログラミングのアプローチを汲んでいる言語のため,そうでない言語に比べてループより再帰の方が記述しやすいという機会が自然と多くなります.慣れておくと良いでしょう.ただしScalaで再帰を書く際には末尾再帰最適化が注意点として挙げられます.末尾再帰最適化というのは,「再帰呼び出しが関数内で最後に評価される箇所でしか発生しない場合に適用される最適化」のことです.この最適化が行われない再帰関数は,容易にスタックオーバーフローを引き起こしてしまうため書くべきではありません.フィボナッチ数列(0,1,1,2,3,5,...と続く,第n項が第n-1項と第n-2項の和で表される数値列)を例にすると以下のようになります.

リスト4.15: 末尾再帰ではないフィボナッチ数列

 1: def fib(n: Int): Int =
 2:   if (n < 2) n else fib(n - 1) + fib(n - 2)

 まず1つ目のコードは末尾再帰最適化が行われないサンプルです.見てみると,if式前半はnを返すだけなので良いものの,後半部分(fib(n - 1) + fib(n - 2))で最後に評価されるのは+になっています(fib(n - 1)fib(n - 2)→ それらの結果の加算を行う+という順で評価される).つまり末尾以外で再帰呼び出しが発生しているため最適化が行われません.

リスト4.16: 末尾再帰のフィボナッチ数列

 1: def fib(n: Int): Int = {
 2:   // 慣習的に再帰のために切り出された内部メソッドには
 3:   // `go` や `loop` という名前が用いられることが多い
 4:   @scala.annotation.tailrec
 5:   def go(n: Int,prev: Int,curr: Int): Int =
 6:     if(n == 0) prev
 7:     else go(n - 1,curr,prev + curr)
 8:   go(n,0,1)
 9: }

 一方2つ目のコードは,全ての再帰呼び出しが最後に評価される箇所(go(n - 1,curr,prev + curr))でしか発生していません.そのためgoメソッドは末尾再帰最適化が行われます.末尾再帰最適化が行われる形への変形は困難な場合もあるため,できないのであればfor式など他の方法を模索しましょう.

 なお,再帰関数を書く場合は@scala.annotation.tailrecというアノテーションを付けておくことで,末尾再帰最適化ができない形になっていたときにコンパイルエラーで教えてくれます.実行時のスタックオーバーフローを防ぐためにも必ず付けておきましょう.

(この項、了)


1. (1 to n)はコレクション(配列やリスト)の一種にあたり,右辺にはそのコレクションを利用できます.詳しくは6章で扱っていきます.

2. ジェネレータを列挙するには基本的には波括弧で囲いますが,ジェネレータが複数行にならないとき(≒1つのとき)のみ,代わりに丸括弧が利用できます.

3. この際にアンダースコア(_)を変数として宣言すると参照できない変数となります.

4. 列挙したパターンが網羅性に欠ける場合(=値によってはどのパターンにもマッチしない可能性がある場合)はコンパイラから警告が出ることがあります(=出ない場合もあります).いずれにせよ,どんな値が来てもいずれかの条件にマッチするようにパターンを構成しましょう.

大阪大学大学院情報科学研究科を修了後、さくらインターネット株式会社にてクラウドサービスのバックエンド開発に従事。プログラミング言語であるScala好きが高じ、Scala関西勉強会やScala関西Summitにて登壇。またその運営にも携わった。 最近はKubernetesと戯れている。

連載バックナンバー

開発ツール書籍・書評
第4回

FizzBuzzしてみよう

2018/8/16
【Scalaはこわくない!マルチパラダイム言語Scalaの入門書!】株式会社インプレスR&Dより発行された「Scalaをはじめよう! ─マルチパラダイム言語への招待─」の立ち読みコーナー第4回です。
システム開発書籍・書評
第3回

Hello Worldを嚙み砕こう

2018/8/14
【Scalaはこわくない!マルチパラダイム言語Scalaの入門書!】株式会社インプレスR&Dより発行された「Scalaをはじめよう! ─マルチパラダイム言語への招待─」の立ち読みコーナー第3回です。
開発言語書籍・書評
第2回

Scalaの環境を作る

2018/8/9
【Scalaはこわくない!マルチパラダイム言語Scalaの入門書!】株式会社インプレスR&Dより発行された「Scalaをはじめよう! ─マルチパラダイム言語への招待─」の立ち読みコーナー第2回です。

Think IT会員サービス無料登録受付中

Think ITでは、より付加価値の高いコンテンツを会員サービスとして提供しています。会員登録を済ませてThink ITのWebサイトにログインすることでさまざまな限定特典を入手できるようになります。

Think IT会員サービスの概要とメリットをチェック

他にもこの記事が読まれています