Help us understand the problem. What is going on with this article?

ES2015でFizzBuzz

More than 1 year has passed since last update.
const range = (s, e) => Array(e - s + 1).fill(0).map((_, i) => s + i)
const fizzbuzz = it => [[3, 'Fi'], [5, 'Bu']]
  .map(([t, s]) => it % t ? '' : s + 'zz')
  .join('') || it
range(1, 15).map(fizzbuzz);
// [1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzBuzz"]

いつものやつです。

ES2015で使える色んなFizzBuzzの実装を考えてみました。
上記の例のようにrange関数、fizzbuzz関数を用意して、
最後にrange(1, 15).map(fizzbuzz)を実行してパコッとくっつける事を想定しています。

ちょっと本来のFizzBuzzの仕様と違いますが、
その辺の差分は次の章(ルール説明)にてご説明します。

※以下の章からは体言止めでしていきますのでびっくりしないように注意してください。

ルール説明

本来の仕様では改行を挟みながら1つずつ出力するが、
今回はFizzBuzzのルールに従った配列を作るものとする。

もし仕様準拠の改行を挟みながら出力していくものへ変換する場合は下記のコードを用いる。
(数値はNumber型のままでも構わない)

// 既にrangeとfizzbuzz関数が用意されている前提
const output = it => console.log(it)
range(1, 15).map(fizzbuzz).forEach(output)
// 1
// 2
// Fizz ...

// Node.jsはこちらも選択肢(下記はChromeでの実行結果)
console.log(range(1, 15).map(fizzbuzz).join('¥n'))
// 1¥n2¥nFizz¥n4¥nBuzz¥nFizz¥n7¥n8¥nFizz¥nBuzz¥n11¥nFizz¥n13¥n14¥nFizzBuzz
  • mapforEachは第二引数がキーになるので、forEach(console.log)としてしまうと悲惨な事になるので注意
  • console.logはリッチなのでfor文でぶん回したり.forEach(it => console.log(it))等とすると凄まじく遅い。Node.js限定だが速度を気にするせっかちな兄貴達は配列にして.join('¥n')を使うと速度面の考慮になる

range関数の選択肢

for文回して普通に作る

コーディング規約上行数が増えやすい傾向があるが、
一般的に使われるのがこれ、速度も非常に速いのが主張。
基本的には以下のどちらかが採用される。

  • for (let i = 0; i < 15; i++)
  • for (let i = 1; i <= 15; i++)

私はループ回数に着眼する場合は前者を利用して、
今の数値に拘る場合は後者を使う風に使い分けしている。
今回はFizzBuzzのように1〜15!みたいな数値が起点になるから後者の方が分かりやすそう。

const range = (s, e, arr = []) => {
  for (let i = s; i <= e; i = i + 1) arr.push(i)
  return arr
}
range(1, 15)
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

亜種はwhileとdo-while
for文だけでチューリング完全なのでこれらの出番はめったに来ない。
いざ面接で書けと言われても多分出てこない…

// while
const range = (s, e, arr = []) => {
  while (s <= e) {
    arr.push(s)
    s++
  }
  return arr
}
range(1, 15)
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

// do-while
const range = (s, e, arr = []) => {
  do {
    arr.push(s)
    s++
  } while (s <= e)
  return arr
}
range(1, 15)
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

Array(num).fill(0)ルート

主張は関数宣言がワンライナー
多少難解ではあるものの、普段から配列のプロトタイプメソッドを使っている人なら普通に読める範疇。
実行速度もforに劣るがオーバーヘッドがArray.prototype.map程度とまずまずの速さ。

因みに他のイディオムでも配列は作れるが、
Array.fromを経由するものは著しく遅いのでこういった関数としてはあまり採用しないほうが良さそう。

const range = (s, e) => Array(e - s + 1).fill(0).map((_, i) => s + i)
range(1, 15)
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

ジェネレータ

こっちも難解だがイディオムと思って書けば十分あり。
しかしジェネレータで配列を作るというのは「お前何やってんだ」感が非常に強い気がする。

const range = (s, e) => [
  ...{[Symbol.iterator]: function* () {
    while (s <= e) yield s++
  }}
]
range(1, 15)
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

再帰関数

再帰関数を使う場合、1行目にガード節を置いて使うと分かりやすい。
速度は大体for文の1.3〜1.5倍程度。

なおJSの再帰関数はUncaught RangeError: Maximum call stack size exceededがすぐに出てくるだらしない言語だが、ブラウザ、Node.js等の実装毎に許容量が異なる。
基本的にどのブラウザの実装も数千が限界値なので、それ以下で収まる事がわかっている場合は積極的に利用してかまわない。

const range = (s, e, arr = []) => {
  if (s > e) return arr
  arr.push(s)
  return range(s + 1, e, arr)
}
range(1, 15)
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

fizzbuzz関数の選択肢

愚直にやる型

第一感これ。
it % 3 === 0と普通に書く方法も有力。

const fizzbuzz = it => {
  if (!(it % 15)) return 'FizzBuzz'
  if (!(it % 3)) return 'Fizz'
  if (!(it % 5)) return 'Buzz'
  return it
}
range(1, 15).map(fizzbuzz)
// [1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzBuzz"]

Switchを使った手法

素直に0と比較してこう。
結構綺麗だと思うんだけど、式を平然とcaseに配置し始める様は結構異端?
コーディング規約で禁止されていたり、エンジニア次第ではかなり嫌われる可能性もあるので注意。

const fizzbuzz = it => {
  switch (0) {
    case it % 15: return 'FizzBuzz'
    case it % 3: return 'Fizz'
    case it % 5: return 'Buzz'
    default: return it
  }
}
range(1, 15).map(fizzbuzz)
// [1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzBuzz"]

switchで式を使う場合は要注意。
falseと比較しないと思わぬ箇所に紛れ込んでしまう不具合が出るようだ。
(これはAltJSのCoffeeScriptの実装を参考にしている)

const isFizz = it => !(it % 3)
const isBuzz = it => !(it % 5)
const isFizzBuzz = it => isFizz(it) && isBuzz(it)
const fizzbuzz = it => {
  switch (false) {
    case !isFizzBuzz(it): return 'FizzBuzz'
    case !isFizz(it): return 'Fizz'
    case !isBuzz(it): return 'Buzz'
    default: return it
  }
}
range(1, 15).map(fizzbuzz)
// [1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzBuzz"]

文字列足し型

ワンライナーやコードゴルフで採用される場合の元のロジックとして使われる奴。
ここから'zz'の部分を共通部品として取り出されたり、(it % 3)を共通化して取り出したりされる。
元ネタはイカさんのツイート

const fizzbuzz = it => {
  let str = ''
  if (!(it % 3)) str += 'Fizz'
  if (!(it % 5)) str += 'Buzz'
  return str || it
}
range(1, 15).map(fizzbuzz)
// [1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzBuzz"]

共通部品を取り出してmapで一気に片付ける作戦

今回の工夫箇所。
割り算の対象(target)と文字列の頭(str)だけ取り出した。
結構読みやすい上、コードがぺたんこになり大満足。

const fizzbuzz = it => [[3, 'Fi'], [5, 'Bu']]
  .map(([t, s]) => it % t ? '' : s + 'zz')
  .join('') || it
range(1, 15).map(fizzbuzz)
// [1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzBuzz"]

計算量は多くなるが[,,,'Fi',,'Bu']としても良い。
その場合はこんな感じのロジックになるが、抽象化し過ぎで流石に読みづらい。

const fizzbuzz = it => [,,,'Fi',,'Bu']
  .map((s,i) => s && !(it % i) ? s + 'zz' : '')
  .join('') || it
range(1, 15).map(fizzbuzz)
// [1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzBuzz"]

普通に足し算

コードゴルフでぐぐったら出てきた
FizzBuzzコードゴルフ - GHOST IN THE WEB
うーん、現状JSでコードゴルフするならこれ一択みたい。

const fizzbuzz = it =>
  (it % 3 ? '' : 'Fizz') + (it % 5 ? '' : 'Buzz') || it
range(1, 15).map(fizzbuzz)
// [1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzBuzz"]

おまけ: ループのあれこれ

たまに面接とかで、FizzBuzzを5通りの手法で書いてね!と言われる事があるかもしれないが、
この手法を使えば4*5で20通りになるので楽勝。

もし怒られが発生したらメソッドチェーン以外の方法を期待している。
その場合はfizzbuzz関数だけ先に宣言しておいて、range関数経由ではなく直接吐き出せば十分だ。

ネタとして下記のコードがサラッと出てくるとかっこいいかも?

ジェネレータ

MDNで調べながら書いたけど、中々可愛くできて満足。
しかし今回のレギュレーションの都合で結局配列生成目的で使われるのが悲しい。

因みにアロー関数とは併用不可、色々機能を省いてるから仕方ないとはいえ残念。

const fb = (s, e) => ({
  [Symbol.iterator]: function* () {
    while (s <= e) yield fizzbuzz(s++)
  }
})
[...fb(1, 15)]
// [1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzBuzz"]

本来の仕様準拠であれば非常に映える。
(しかし、console.logの速度が悪すぎて普通に配列作ってjoinで固めた方が速いあたりが不憫)

const range = (s, e) => ({
  [Symbol.iterator]: function* () {
    while (s <= e) yield s++
  }
})
for(const it of range(1, 15)) console.log(fizzbuzz(it))
// 1
// 2
// Fizz ...

プロトタイプメソッド拡張

まさかFizzBuzzのためにプロトタイプメソッドを拡張し始める事になるとは思わなんだ…
見てくれ以外は全部上記の組み合わせだけど、異なるロジックに含まれるのだろうか。

また実践ではメソッド名が衝突する可能性が高いので多用厳禁。

Object.defineProperty(Array.prototype, 'fizzbuzz', {
  configurable: true,
  writable: true,
  value: function () {
    return this.map(it => [[3, 'Fi'], [5, 'Bu']]
      .map(([t, s]) => it % t ? '' : s + 'zz')
      .join('') || it
    )
  }
})
Array(15).fill(0).map((_, i) => i + 1).fizzbuzz()
// [1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzBuzz"]

高階関数

高階関数を使った関数型プログラミング的な手法。
JavaScriptは関数が第一級オブジェクトであるので、関数実行時の引数や戻り値に設定することが出来る。

中身は結局メソッドチェーンではある。

const map = fn => arr => arr.map(fn)
map(fizzbuzz)(Array(15).fill(0).map((_, i) => i + 1))
// [1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzBuzz"]

高階関数 with パイプライン演算子

ES.nextで提案されているパイプライン演算子と併用するともう少し読みやすくなる。
実装は未定で、早くても2020年位…対応ブラウザも現時点では全く存在しない。
まぁ、mapだけなら無理に使わずともメソッドチェーンで事足りるがロマンはある。

const map = fn => arr => arr.map(fn)
Array(15).fill(0)
|> map((_, i) => i + 1)
|> map(fizzbuzz)
// こうなるはず: [1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzBuzz"]

おまけのおまけ: カリー化

今回の高階関数ではmap関数を用意する時にお手製の手抜きカリー化を施したが、
他のライブラリでは引数を1個ずつ与えても、2個以上一気に与えても正常に動作する。

JavaScriptの関数はlengthプロパティを参照すれば要求する引数の数が取得出来る。
その為、引数が要求数以上溜まるまで関数の発火を行わず遅延させる関数を用意して包めばカリー化になる。

もし自力で実装するなら大体こんな感じになる。

const curry = (fn, ...args) =>
  args.length >= fn.length
    ? fn(...args)
    : (...its) => curry(fn, ...args, ...its)
const add = curry((a, b) => a + b)
add(1, 2) // 3
add(1)(2) // 3

LodashRamda.js等のライブラリもcurryというメソッドを用意している。

これらのライブラリは関数の発火を更に遅延して、
第二引数だけ先に束縛して、第一引数を待つ関数を生成出来るので表現力が桁違いに高い。
遅延は流石に数行じゃ書けないので、上記ライブラリを頼った方が良いと思う。

miyabisun
備忘録をぼちぼち残しておく系エンジニア PHPerからNoderになりました(っ╹◡╹)っ
http://www.romancing.jp/
xcoo
ゲノムおよび生体情報解析のトータルソリューション「Chrovis」の開発をしています。
https://xcoo.co.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした