🧩

リテラルなしでFizzBuzzを実装する

2021/04/18に公開

大喜利のお題

リテラル(literal)ってなに?

リテラルを直訳すると「直値」ですが、それも普段聞き慣れない言葉ですね。
分かりやすく表現すると、コード内に直書きされた数値や文字列などの定数です。
いずれも表記が直接自身を示す値なので、「直値」というわけです。

FizzBuzz(フィズ・バズ)ってなに?

FizzBuzzとは言葉遊びの一種です。数を数えながら特定のルールで別の言葉に言い替えます。
プログラミングの世界では、Hello worldなどに並ぶ手習いの題材として親しまれています。

# input
 1  2  3  4  5  6  7  8  9 10
11 12 13 14 15 16 17 18 19 20
21 22 23 24 25 26 27 28 29 30
31 ...

# output
 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 ...

基本的なルールは次の通りです。

  • 1から順に、数字を一つずつ増やしながら出力する
  • 数字が3で割り切れる場合は、数字の代わりに「Fizz」と出力する
  • 数字が5で割り切れる場合は、数字の代わりに「Buzz」と出力する
  • 数字が3と5で割り切れる場合は、数字の代わりに「FizzBuzz」と出力する

単純に実装すると次のようになります。

FizzBuzz実装例(リテラルあり)
fizzbuzz_with_literal.go
package main

import "fmt"

func main() {
  for n := 1; n <= 31; n++ {
    put_fizzbuzz_or_num(n)
    put_delimiter(n)
  }
  fmt.Println()
}

func put_fizzbuzz_or_num(n int) {
  switch {
  case can_div(n, 15):
    fmt.Print("FizzBuzz")
  case can_div(n, 3):
    fmt.Print("Fizz")
  case can_div(n, 5):
    fmt.Print("Buzz")
  default:
    fmt.Print(n)
  }
}

func put_delimiter(n int) {
  switch {
  case can_div(n, 10):
    fmt.Println()
  default:
    fmt.Print(" ")
  }
}

func can_div(n int, d int) bool {
  return n%d == 0
}

https://play.golang.org/p/Vut8z6f4JY2

リテラルなしでFizzBuzz書けるの?

はい、書けます。
言語仕様を駆使して必要な数値や文字列を動的に生成すれば良いのです。

FizzBuzz実装例(リテラルなし)
fizzbuzz_without_literal.go
package main

import (
  "fmt"
  "reflect"
)

type Fizz int
type Buzz int

var fizz Fizz
var buzz Buzz

const (
  N0  = iota         // 0: ゼロ
  N1                 // 1: 順列の開始番号
  N3  = N1 + N1 + N1 // 3: Fizz出力判定用
  N5  = N3 + N1 + N1 // 5: Buzz出力判定用
  N10 = N5 + N5      // 10: 改行出力判定用
  N31 = N10*N3 + N1  // 31: 順列の終了番号
  N32 = N31 + N1     // 32: 半角空白のASCIIコード
)

func main() {
  fizzbuzz()
}

func fizzbuzz() {
  FIZZ := reflect.TypeOf(fizz).Name() // Fizz: 文字列
  BUZZ := reflect.TypeOf(buzz).Name() // Buzz: 文字列
  LINE_FEED := fmt.Sprintln()         // (LF): 改行
  WHITE_SPACE := string(rune(N32))    // (SP): 半角空白
  for n := N1; n <= N31; n++ {
    ifput(n%N3 == N0, FIZZ)
    ifput(n%N5 == N0, BUZZ)
    ifput(n%N3 != N0 && n%N5 != 0, fmt.Sprint(n))
    ifput(n%N10 == 0, LINE_FEED)
    ifput(n%N10 != 0, WHITE_SPACE)
  }
  fmt.Println()
}

func ifput(eval bool, put string) {
  if eval {
    fmt.Print(put)
  }
}

https://play.golang.org/p/6VPVAHbAdIn

数値を生成してみる

今回生成する必要がある数値は以下のとおりです。

  • 入力順列の先頭と末尾の番号(1, 31 ※末尾の番号は任意)
  • 割り切れるかどうかの判定に用いる除数(3, 5, 15=3*5)

いずれも自然数ですので、整数の1が用意できれば、それを元手に演算生成できますね。

手法1. 1加算演算子で"0"から"1"を生み出す

大抵の言語では整数型の変数の未定義初期値は0です。
おなじく、大抵の言語では、整数型に対する1加算演算子が用意されています。

そこで、1加算演算子を用いて1を作り、そこから加積算で各数値を生成します。

gen-num-from-no1.go
package main

import "fmt"

func main() {
  var N1 int // default: 0
  N1++
  var N2 = N1 + N1
  var N3 = N2 + N1
  var N5 = N2 + N3
  var N15 = N3 * N5
  var N31 = N15 * N2 + N1
  fmt.Println(N1, N2, N3, N5, N15, N31)
  // stdout: 1 2 3 5 15 31
}

https://play.golang.org/p/WZ1Jie4mIQY

手法2. 配列の長さから数値を生み出す

大抵の言語では配列の要素数を返す関数があります。
要素1の配列の長さを求めて1を作り、そこから演算で格数値を生成します。

同じ加積算では芸がないので、今度はビット演算を用いてみました。

gen-num-from-len.go
package main

import "fmt"

func main() {
  var N1 int
  N1 = len([]int{N1})
  var N2 = N1 << N1
  var N3 = N2 | N1
  var N5 = N2 << N1 | N1
  var N15 = N3 << N2 | N3
  var N31 = N15 << N1 | N1
  fmt.Println(N1, N2, N3, N5, N15, N31)
  // stdout: 1 2 3 5 15 31
}

https://play.golang.org/p/xI9UZowuXVA

文字列を生成してみる

手法1. 数値から文字を生み出す

今回生成する必要がある文字列は"Fizz"と"Buzz"の2つです。
どちらもASCIIに含まれる文字の組み合わせです。

ASCIIは7桁の2進数、言い替えれば0〜127の整数に、英数字や記号を割り当てた文字コードです。
そして大抵の言語ではASCIIを数値表現と文字表現の両方で扱える標準関数を提供しています。

つまり、以下の流れで"Fizz"と"Buzz"は用意できるわけですね。

  1. ASCII文字に対応する数値を用意する(前節「数値を生成してみる」参照)
  2. 言語の標準関数を用いて、ASCIIの数値表現から文字表現に変換する
  3. 個々の文字を結合して、必要な文字列を構築する

手法2. リフレクションで文字列を抽出する

幾つかの言語においては、リフレクションと呼ばれる仕組みが提供されています。
リフレクションを用いるとプログラムの構造を取得できます。

例えば以下のように型名を取得し、文字列として扱うことが可能になるわけです。

gen-str-with-reflection.go
package main

import (
  "fmt"
  "reflect"
)

type FizzBuzz *interface{}

func main() {
  var v FizzBuzz
  fmt.Println(reflect.TypeOf(v).Name())
  // stdout: FizzBuzz
}

https://play.golang.org/p/3OP5QPrPbvz

さいごに

普段は可読性の観点から推奨されないパズルめいた実装も楽しいものですね。
皆さんも各々の好きな言語で試してみてはいかがでしょうか?
もしかすると、何か新たな発見や気づきがあるかもしれません。

Discussion