LoginSignup
6
2

More than 3 years have passed since last update.

【Ruby】より"よい" FizzBuzzを書く

Last updated at Posted at 2019-06-19

はじめに

自分でルールを拡張できるFizzBuzzを、より"よく"書きます。
ダブルクオートがついているのは、「よさとは一体……」という深淵に対する畏怖の現れです。

コードのよさは個別の状況によって変わるものです。
さまざまな評価軸を知り、それぞれのよさを高める書き方・考え方を身につけましょう。

対象読者

  • (前提) Ruby で一応 FizzBuzz を書ける人
  • より"よい"コードを書きたい人

問題

標準入力から以下のように実行できる fizzbuzz.rb を作成します。

"3:Fizz" のような文字列がいくつか与えられ、最後にFizzBuzzの対象となる数値が入力されます。

ruby fizzbuzz.rb 3:Fizz 5:Buzz 25
Buzz

ruby fizzbuzz.rb 3:Fizz 5:Buzz 15
FizzBuzz

ruby fizzbuzz.rb 3:Fizz 5:Buzz 7:Guzz 35
BuzzGuzz

ruby fizzbuzz.rb 3:Fizz 5:Buzz 13
13

追記: 0をルールとして含めた場合の結果は特に指定しません。

ruby fizzbuzz.rb 0:Zero 0

前置き コードのよさ

解答のまえに、コードのよさについてちょっと考えます。

本記事は「良いコードをかくぞ!」という気持ちになってもらうのが狙いです。

網羅的に書くのはすごい本たちにまかせます。

👇すごい本たち

コードのバリエーション

ある問題を解決するコードには無数のバリエーションがあります。

全体を貫くアルゴリズムによる違いもありますが、こまかい文法による違いもあります。

後者については、たとえば、以下のように fizzbuzz の結果を戻り値にしたいとき。

def fizzbuzz(number)
  #...

  if result == ""
    return number
  else
    return result
  end
end

これは、Rubyの if が文ではなく式であることを知っていればこう書けます。

def fizzbuzz(number)
  #...

  return if result == ""
    number
  else
    result
  end
end

さらに、Rubyのメソッドは必ず最後の値を戻り値にするので、 return が省略できます。

def fizzbuzz(number)
  #...

  if result == ""
    number
  else
    result
  end
end

さらにさらに、今回なら三項演算子を使うのもいいでしょう。

def fizzbuzz(number)
  #...

  result == "" ? number : result
end

条件を反転させることもできます。

def fizzbuzz(number)
  #...

  result != "" ? result : number
end

String クラスの empty? メソッドを使うという手もあります。

def fizzbuzz(number)
  #...

  result.empty? ? number : result
end

このように、同じ内容でも書き方はさまざまあります。

これらのコードの優劣は、コード単体では評価できません。
つまり、いついかなるときもこの書き方はこの書き方よりよい、なんてことはありません。

たとえば、三項演算子は短くかけて便利ですが、知らない人に対してはコードをいかつく見せる機能をもちます。
僕が初学者に見せるコードを書くなら、いきなり三項演算子は使わないでしょう。

empty? メソッドも、 String クラスに empty? メソッドがあると知らなければ読みにくくさせてしまうかもしれません。

コードの評価軸

上にみた「いかつさ」のようなコードの評価軸はたくさんあります。

  • 実行速度
  • メモリ使用量
  • 可読性
  • 拡張性
  • 安全性
  • 堅牢性
  • テスト可能性
  • 凝集度と結合度

empty? メソッドや三項演算子は、人によって可読性がかわります。

コードそのもののなかに「読みにくさ」や「遅さ」があるわけではありません。

このようにコードは、「誰が読むのか」などの個別の状況を踏まえて評価されます。

  • コードを読む
  • コードを使う期間
  • コードを使う頻度
  • 入力される値の範囲
  • 不正な入力の可能性

などなど……。

言語選択

上にあげたコードの評価にもっとも覆しがたく影響を与える「状況」は、選択する言語です。

たとえばRubyは、C言語とは比べ物にならないくらい実行速度が遅いです。
逆立ちしてもかなわないので、実行速度が大事ならRubyで書くのがそもそも間違いです。

だからといって、RubyがC言語の下位互換というわけではありません。
評価軸はたくさんあるのです。

そんな「遅い」Rubyの強みは、ずばり、可読性開発速度です。
Rubyで書くのなら、このふたつを意識しましょう。

Rubyの長所短所については、たくさんの人がすばらしい記事を書いています。
特に以下のQuoraの回答はおすすめです。

👉なぜrubyは他の言語と比べて遅いのでしょうか?

よいコードへの道は、言語そのものについて知るところから始まります。

解答例

前置きが長くなりましたが、問題の解答例です。

def main(argv)
    m = argv.last.to_i
    argv.delete_at(-1)
    a = []
    s = []
    argv.each do |v|
        a << v.split(":")[0].to_i
        s << v.split(":")[1]
    end

    output = ""
    a.each.with_index do |a, i|
        output << s[i] if m % a == 0
    end

    if output == ""
        return m
    else
        return output
    end
end

puts main(ARGV)

ツッコミどころ リファクタリングのやりがいがたくさんあります。

個別の評価軸について

評価は評価軸ごとに独立のものではなく、可読性を上げると実行速度が下がるといったように、互いに結びついています。

状況に応じたチューニングをする前に、個別の評価軸ごとにできることを考えます。

評価軸はたくさんあってキリがないので、以下の点に絞ります。

  • 可読性
  • 実行速度
  • 拡張性
  • 安全性

可読性

インデントはスペースふたつ

Rubyの慣習的に、インデントはスペースふたつです。

強制ではないから従わなくてもいい、というものではありません。
読み慣れたコードの見た目をしていないだけで脳は負荷を感じ、可読性は下がるのです。

エディタの設定を見直して、必ずインデントをスペースふたつにしましょう。

適切なメソッド名をつける

mainは、C言語などでは意味のある名前ですが、Rubyでは必要ありません。1

今回は fizzbuzz のためのメソッドなので、素直にfizzbuzzとしておきましょう。

def fizzbuzz(argv)
  # ...
end

変数名は内容をあらわすものを

ma のような変数名は、「短く書く努力」ではなく「適切な命名の怠慢」です。
十分に体をあらわす名をつけましょう。

命名の妥当性を判断する方法としては、その一行だけを読んで意味がわかるか?をチェックすると良いです。

たとえばこの一行。
mが何のための変数なのかわかりません。

m = argv.last.to_i

コードの意味がわかるようにする一つの手は、コメントを書くことです。

# m: fizzbuzzの対象となる整数値
m = argv.last.to_i

コメントは雄弁で、なんでも伝えることができますが、それだけ書く手間と読む手間を要求します。

コメントを書く暇があったら、わかりやすい命名を考えましょう。

target = argv.last.to_i

適切な命名ができないとき

argv.each do |v|
  a << v.split(":")[0].to_i
  s << v.split(":")[1]
end

このうち、以下の部分について…。

a << v.split(":")[0].to_i

このavにも適切な名前をつけたいのですが、なかなか思いつきません。

名前をつけにくいときには、以下の状況が考えられます。

  • 一行で多くのことをやりすぎている
  • そもそもまわりのコードがおかしい

順に見ていきます。

一行で多くのことをやりすぎている

a << v.split(":")[0].to_i

この一行では

  1. 文字列を:で区切り、
  2. その0番目をとりだし、
  3. 整数に変換し、
  4. 配列aの最後尾にいれる

なんと4つもの仕事をこなしています。

名前をつけられるレベルまで分解しましょう。

argv.each do |rule|
  splitted = rule.split(":")
  numbers << splitted[0].to_i
  strings << splitted[1]
end

さらに、splittedのような一時変数を意識したくないので、多重代入を使いましょう。

argv.each do |rule|
  num, str = rule.split(":")
  numbers << num.to_i
  strings << str
end

そもそもまわりのコードがおかしい

"3:Fizz"という、FizzBuzzのルールにおいて、数字の3と文字列のFizzは密接な関係にあります。
関係をこわして数字だけ配列にいれるというような操作は、そもそもするべきではありません。

より意味を表した操作をしましょう。

rules = []
argv.each do |rule|
  num, str = rule.split(":")
  rules << [ num.to_i, str ]
end

適切なメソッドをつかう

標準で用意されているメソッドの使い所を知りましょう。

以下の場面ではArray#popメソッドが使えます。

# bad
target = argv.last.to_i
argv.delete_at(-1)

# good
target = argv.pop.to_i

popは、もともと配列をスタックとして使うために用意されたメソッドです。

popメソッドを言葉で説明するなら、
「自身の末尾から要素を取り除き、取り除いた要素を返すメソッド」です。
なんだか複雑そうにみえます。

しかし、一度スタックという概念と結びついているんだとわかれば、スタックもpopも忘れないでしょう。

概念同士の結びつきは、お互いの記憶を長持ちさせます。
次からは自由に使いこなせるはずです。

メソッドや文法を学ぶときには、ぜひその歴史や存在意義を調べてください

実行速度

ずばり計算量を意識しましょう。
計算量とは、アルゴリズムの実行速度がざっくりどのくらいかを試算するための値です。

理論的に考えはじめるとわりと面倒なので、ざっくりわかればいいです。

とりあえず、以下の2つに気をつけましょう。

  • ループ
  • 配列とハッシュの違い

ループ

ループとは、配列などの要素にたいして順番に処理をおこなうことです。
たとえば、Array.eachArray.mapはループです。

ループの重要な特徴は、ループの対象となる要素が増えれば増えるほど実行時間が長くなることです(当然です)。
ですから、ループ回数は減らしたほうが速いです。

今回の例では、ループが2ヶ所で実行されています。

def fizzbuzz(argv)
  # ...

  # 標準入力からルールを取り出すループ
  argv.each do |v|
    # ...
  end

  # 結果を計算するループ
  a.each.with_index do |a, i|
    # ...
  end

  # ...
end

ループを減らしてみます。

def fizzbuzz(argv)
  # ...

  result = ''

  # 標準入力からルールを取り出し、結果を計算するループ
  argv.each do |rule|
    num, str = rule.split(':')
    result << str if target % num.to_i == 0
  end

  # ...
end

すっきりしました。

ここで、Array#reduceを使うこともできます。

def fizzbuzz(argv)
  # ...

  # 標準入力からルールを取り出し、結果を計算するループ
  result = argv.reduce('') do |res, rule|
    num, str = rule.split(':')
    res << str if target % num.to_i == 0
    res
  end

  # ...
end

Array#reduceの動きはすこしわかりにくいですね。
Array#eachではなくArray#reduceを使うメリット(デメリット)は何でしょうか?
調べてみてください。

まとめると、ループは少ないほうが実行速度が早くなります。
また一般に、ループの中でやることが増えるとコードの可読性が下がります。
バランスが肝心です。

配列とハッシュの違い

データを入れているのが配列かハッシュかで、実行速度に大きくかわる場合があります。

いくつかのルールの中で7に対応する文字列を調べてみます。

配列の場合
rules = [[3, 'Fizz'], [5, 'Buzz'],  [7, 'Bizz']]

rules.each do |rule|
  if rule[0] == 7
    puts rule[1] 
    break
  end
end
ハッシュの場合
rules = {
  3 => 'Fizz',
  5 => 'Buzz',
  7 => 'Bizz'
}

puts rules[7]

最大の違いはループがあるかないかです。

ハッシュは配列と違って、7に対応する値をとりだすためにループを必要としません。
キーがわかれば、すぐにそれに対応する値にアクセスできるのです。

キーとなる値をもとに取り出すようなデータは、配列でも表現できますが、実行速度の観点からみればハッシュ一択です。

複数の値を保持しておくものには、配列やハッシュのほかにもいくつかの形式があります。
これらの形式のことをデータ構造といいます。

データ構造にはほかにもキューやスタック、リスト、グラフなどがあります。
配列やハッシュに比べると、より限定的な目的のために特化したデータ構造です。

データ構造は、実行速度だけでなくメモリ使用量にも大きく影響します。
しかし、Rubyを使う以上どちらも気にしすぎることはないでしょう。

ここまでのまとめ

ここまでの内容をまとめると、たとえば以下のようなコードになります。

def fizzbuzz(argv)
  target = argv.pop.to_i

  result = ''
  argv.each do |rule|
    num, str = rule.split(":")
    result << str if target % num.to_i == 0
  end

  result == '' ? target : result
end

puts fizzbuzz(ARGV)

これでも問題の解答としては十分ですが、つづけて他の評価軸についても検討してみましょう。

拡張性

拡張性とは、コードの変更のしやすさです。

そのためには不要な依存関係を断ち切ることが必要です。

入力形式に依存させない

引数が ["3:Fizz", "5:Buzz", 15] のような特殊な入力形式に依存しているのは拡張性がさがるポイントです。
コードの可読性からみても望ましくありません。

そこで、FizzBuzzを解くメソッドと、標準入力を分解するアダプターとしてのメソッドを分離する方法があります。

# より使いやすい入力形式を受け取るfizzbuzz
# fizzbuzz( 15, { 3=>'Fizz', 5=>'Buzz', ...} )
def fizzbuzz(target, rules)
  result = ''
  rules.each do |num, str| # RubyのHash#eachは順番が保証される
    result << str if target % num == 0
  end

  result == '' ? target : result
end

# 標準入力のargvをfizzbuzzメソッドにあわせるアダプター
def fizzbuzz_argv(argv)
  target = argv.pop.to_i
  rules = {}
  argv.each do |x|
    num, str = x.split(':')
    rules[ num.to_i ] = str
  end

  fizzbuzz(target, rules)
end

puts fizzbuzz_argv(ARGV)

これにより、fizzbuzzメソッドは息の長いメソッドになります。
今後、FizzBuzzを計算するウェブサイトを作ることになったとして、このfizzbuzzメソッドはなんの変更もなく使い回せるでしょう。

また、コード全体としてループの回数が増えた点は興味深いです。
これも実行速度とのトレードオフになっています。

クラスを定義する

fizzbuzz をとことん抽象化すると、「数字を入力して、ルールにもとづいて文字か数字を出力するメソッド」です。

倍数しかルールとして登録できないのは、拡張性が低いですね。

自分の好きなルールを登録できるようにしましょう!

Rubyならクラスを定義するのがいいでしょう。
マジで定義するのは面倒なので読者への宿題とします😤

class FizzBuzz
  # ...
end

fizzbuzz = FizzBuzz.new
fizzbuzz.multiple_of(3, 'Fizz')
fizzbuzz.multiple_of(5, 'Buzz')
fizzbuzz.divisor_of(360, 'Divv') # 360の約数

fizzbuzz.culc(15) # FizzBuzzDivv

また、入力形式にあわせたイニシャライザを用意することもできます。

class FizzBuzz
  # ...
end

fizzbuzz = FizzBuzz.from(argv: argv)

fizzbuzz.culc(15) # FizzBuzzDivv

安全性

安全性とは、エラーの生じにくさ、あるいはエラーを適切にハンドリングできている度合いです。

def fizzbuzz(target, rules)
  result = ''
  rules.each do |num, str|
    result << str if target % num == 0
  end

  result == '' ? target : result
end

さきほど入力形式に依存しないように変更したfizzbuzzメソッドです。

このメソッドは第一引数に数字がはいることを想定していますが、間違えて文字列を入れてしまいました。

すると・・・。


puts fizzbuzz( 15, { 3=>'Fizz', 5=>'Buzz' })
# FizzBuzz

puts fizzbuzz( '15', { 3=>'Fizz', 5=>'Buzz' })
# 15

結果は15になりました。
これは数字の15ではなく文字列の"15"です。
いずれにせよ、望ましくない結果です。

Rubyは動的型付け言語なので、引数に予期せぬ値が入るということは往々にしてあります。

間違った引数を与えた場合にどう振る舞うかを考えておくべきでしょう。

方針としては2つあります。

  • エラーを出す
  • エラーを出さない

(驚くことに、すべての方針はこの2つに分類できます!)

エラーを出す

俺が決めた挙動以外はみとめない。
そんな場合はエラーを出しましょう。

以下では、引数のクラスを判定し、それぞれエラーを出しています。

def fizzbuzz(target, rules)
  unless target.kind_of? Integer
    raise ArgumentError, 'targetの値はIntegerを継承したクラスのインスタンスだけ!'
  end
  unless rules.kind_of? Hash
    raise ArgumentError, 'rulesの値はHashを継承したクラスのインスタンスだけ!'
  end

  result = ''
  rules.each do |num, str|
    result << str if target.to_i % num == 0
  end

  result == '' ? target : result
end

エラーを出さない

エラーを出さずに、できる限り違う入力にも対応するという方針です。
ちょっとややこしいですが、Rubyらしい方法です。

def fizzbuzz(target, rules)
  result = ''
  rules.each do |num, str|
    result << str if target.to_i % num == 0 # target が target.to_i になった
  end

  result == '' ? target : result
end

target.to_iになりました。

これにより、targetに文字列を入れてもfizzbuzzを実行できるようになりました。

puts fizzbuzz('15', { 3=>'Fizz', 5=>'Buzz' })
# FizzBuzz

String#to_iInteger#to_iがそれぞれ実装されているからです。
target.to_iが実行される段階では、targetが文字列ならString#to_iが、整数ならInteger#to_iが呼び出されます。

つまり、整数に変換するto_iメソッドを呼び出すことができるオブジェクトならなんでもtargetにすることができます。

def fizzbuzz(target, rules)
  # ...
end

class Box
  attr_accessor :item, :count

  def initialize(item, count)
    @item = item
    @count = count
  end

  def to_i
    @count
  end
end

box = Box.new('りんご', 15)

puts fizzbuzz(box, { 3=>'Fizz', 5=>'Buzz' })
# FizzBuzz

Boxクラスは、あるアイテムをいくつか入れておけるクラスです。

このクラスにはto_iメソッドを定義してあるので、fizzbuzzメソッドはその内部でbox15に変換します。
見事FizzBuzzという結果を得られました。2

Rubyでは、あるメソッドが呼び出せるかどうかは、呼び出した瞬間に判定されます。
実際に実行されるのがString#to_iだろうとInteger#to_iだろうとBox#to_iだろうと、とにかくto_iというメソッドが実行できればいいのです。
to_iが期待するのは、ただ整数値に変換できること、それだけです。

オブジェクトのクラスよりも、オブジェクトがメソッド呼び出しに応えられるかが肝心です。

このような考えのもとプログラムを書くことを、ダックタイピングといいます。
「それがアヒルかどうかよりも、アヒルのように鳴くかどうかが肝心」ということです。

ダックタイピングをうまく行えば、Rubyらしく柔軟なコードが書けるようになります。

個別の状況について

これは本当にさまざまです。

状況の判断材料を再掲します。

  • コードを読む
  • コードを使う期間
  • コードを使う頻度
  • 入力される値の範囲
  • 不正な入力の可能性

コードを読む人

どれくらいコードが書ける人が読むのかによって、書くべき内容は変わります。
初心者にArray#reduceなど見せても混乱するだけです。

少なくとも、読んでもらう必要があるコードはできるだけ可読性の高いコードにすべきです。
他人だけではなく自分もコード読むことに注意してください。

書くときは謙虚に、読むときは傲慢に
これは今思いついた名言です。

プログラマというものは、よりテクニカルに問題を解決しようとしたがるものです。

しかし、よりよいコードを書く人はいくらでもいます。
「自分には最善のコードは書けない」と謙虚になりましょう。

あとで自分が読んだときにわからなくなる可能性もあります。
そのときはなんとしても読み解いて、その場でリファクタリングするのです。
「自分はよりよいコードに改善できる」と、傲慢になりましょう。

コードを使う期間

コードを使う期間が長いほど、特に拡張性が重要になります。
思わぬ変更はすぐにやってきます。

逆に、コードを使う期間が短いなら、正常に動きさえすればぐっちゃぐちゃのコードでも構いません(と言い切るのもなんですが)。

コードを使う頻度

頻繁に使うコードなら、実行速度が重要になるかもしれません。
1日10回実行するコードの実行時間をたった1秒短縮するだけで、1年間で1時間の短縮になります。

入力される値の範囲

10万件のデータをあつかうような場合は、計算量メモリの消費量についてよく検討する必要があります。

不正な入力の可能性

コードの安全性が問題になります。

コードを一般に公開する場合や、いろんな場所でコードを使い回す場合にとくに注意しましょう。

まとめ

FizzBuzzを題材に、いいコードについて考えました。

コードにはたくさんの評価軸があり、互いに複雑にからまりあっています。
評価軸ごとに最善のコードを考え、さらに評価軸同士のバランスをとりましょう。

また、いいコードは個別の状況に応じて変わるものです。
コードに何が求められているのかを判断できるようになりましょう。

この記事をきっかけに、コードを見る目がかわったら嬉しいです!


  1. Rubyでmainといえば、トップレベルのselfのことです。 

  2. fizzbuzz(box.count, { 3=>'Fizz', 5=>'Buzz' })のように書けばいいじゃん、というのは野暮です。 

6
2
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2