似非プログラマのうんちく

「似非プログラマの覚え書き」出張版

Scala でとことん FizzBuzz する(その 3)

さぁ、改造を続けよう。

FizzBuzz アルゴリズム再考

ちょっとひねって、次のような写像を考える。

何も書いていないところは空文字列を対応させていると考える。これを利用して、FizzBuzzアルゴリズムを少し変更する。

  1. 何らかの方法で "", "", "Fizz", "", "Buzz", "Fizz", "", "", "Fizz", "Buzz", "", "Fizz", "", "", "FizzBuzz", ... と繰り返される列を用意する。
  2. 1 から順に上記の列と対応させていく(zip)
  3. 上記で得られた列を順に処理していくとき、「数字と空文字列」の組み合わせのときは数字を、「数字と空でない文字列」の組み合わせのときは文字列を返すようにする。

これを Scalaトレイトを利用して抽象クラスとしてあらかじめ作っておく。

package jp.mydns.akanekodou.scala.fizzbuzz

abstract class FizzBuzz {
  protected val fb : Stream[String]

  def fizzbuzz : Unit = {
    1 to 100 zip fb map {
      case (n, "") => n
      case (_, s) => s
    } foreach println
  }
}

Java のインターフェースと違い、Scala のトレイトにはこうした共通処理をあらかじめ実装しておくことが出来る。今回、ついでなので表示するところまでまとめて実装してみた。

2015/04/12 追記 : 今回のケースではトレイトを使う必要はありませんでした。お詫びの上、通常の抽象クラスに訂正させていただきます。

もう一つ、Scala の機能である「暗黙引数」を利用している。一見するとメソッドの引数のデフォルト値をあらかじめ与えるのと似ているが、あちらはあらかじめ引数を初期化しておかないとダメなのに対し、こちらはメソッド呼び出し時までに暗黙引数を初期化・確定させておけば良い。

Java の抽象クラスは「抽象メソッドが含まれるクラス」であるが、Scala の場合は抽象メソッドに加えて上記のような抽象フィールドを含むクラスも抽象クラスとして定義しなければならない。

2015/04/12 追記 2 : MainFizzBuzz 系クラスを継承させる方法が何となく行儀が悪く見えたので止めました。

その他のファイルも適宜書き換えていこう。

package jp.mydns.akanekodou.scala.fizzbuzz

class FirstFizzBuzz extends FizzBuzz {
  def toFizzBuzz(n : Int) : String = {
    if (n % 3 == 0) {
      if (n % 5 == 0)
        "FizzBuzz"
      else
        "Fizz"
    } else if (n % 5 == 0) {
      "Buzz"
    } else {
      ""
    }
  }

  val fb = Stream.from(1).map(toFizzBuzz)
}

toFizzBuzz は最早 String 型しか返さないことが明白になった。

package jp.mydns.akanekodou.scala

import jp.mydns.akanekodou.scala.fizzbuzz._

object Main extends App {
  (new FirstFizzBuzz).fizzbuzz
}

Main.scala がだいぶすっきりした。

いよいよ本格的に改造していくぞ !

分岐アルゴリズム再考

その 1 : 割り切れチェックを 1 回ずつで済ませる

何はなくとも 3 の倍数のときは "Fizz" と言うことは確定している。"Buzz" が付くか付かないかはともかくとして。そう考えるとこんな書き方でも良いはずだ。

package jp.mydns.akanekodou.scala.fizzbuzz

class SecondFizzBuzz extends FizzBuzz {
  def toFizzBuzz(n : Int) : String = {
    var str = ""
    if (n % 3 == 0) str += "Fizz"
    if (n % 5 == 0) str += "Buzz"
    str
  }

  val fb = Stream.from(1).map(toFizzBuzz)
}

空文字列を用意して、まず 3 の倍数だったらそこに "Fizz" を追加する。5 の倍数だったら "Buzz" を追加する。3 の倍数かつ 5 の倍数なら両方が追加される。FizzBuzzWoof のような拡張に対しては

if (n % 7 == 0) str += "Woof"

を追加すれば良いので拡張にも対応できる。

package jp.mydns.akanekodou.scala

import jp.mydns.akanekodou.scala.fizzbuzz._

object Main extends App {
  (new SecondFizzBuzz).fizzbuzz
}

その 2 : パターンマッチを使う

パターンマッチを使うとこんな書き方も出来る。

package jp.mydns.akanekodou.scala.fizzbuzz

class PatternMatchFizzBuzz extends FizzBuzz {
  def toFizzBuzz(n : Int) : String = {
    (n % 3, n % 5) match {
      case (0, 0) => "FizzBuzz"
      case (0, _) => "Fizz"
      case (_, 0) => "Buzz"
      case _      => ""
    }
  }

  val fb = Stream.from(1).map(toFizzBuzz)
}

case は上から順にチェックして最初にマッチしたものが実行されるので、まず最初に 3 と 5 の両方で割り切れるパターンをチェックして、それからどちらか一方だけで割り切れるパターン、最後にそれ以外、とする。この書き方は FizzBuzzWoof への拡張に対してはやや弱い(マッチパターンが一気に増えるため)。

package jp.mydns.akanekodou.scala

import jp.mydns.akanekodou.scala.fizzbuzz._

object Main extends App {
  (new PatternMatchFizzBuzz).fizzbuzz
}


次回もますます不適切(?)に改造していくよ !