はじめに
自分でルールを拡張できる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
前置き コードのよさ
解答のまえに、コードのよさについてちょっと考えます。
本記事は「良いコードをかくぞ!」という気持ちになってもらうのが狙いです。
網羅的に書くのはすごい本たちにまかせます。
👇すごい本たち
- リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)
- UNIXという考え方―その設計思想と哲学
- プリンシプル オブ プログラミング3年目までに身につけたい一生役立つ101の原理原則
コードのバリエーション
ある問題を解決するコードには無数のバリエーションがあります。
全体を貫くアルゴリズムによる違いもありますが、こまかい文法による違いもあります。
後者については、たとえば、以下のように 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の回答はおすすめです。
よいコードへの道は、言語そのものについて知るところから始まります。
解答例
前置きが長くなりましたが、問題の解答例です。
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
変数名は内容をあらわすものを
m
や a
のような変数名は、「短く書く努力」ではなく「適切な命名の怠慢」です。
十分に体をあらわす名をつけましょう。
命名の妥当性を判断する方法としては、その一行だけを読んで意味がわかるか?をチェックすると良いです。
たとえばこの一行。
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
このa
やv
にも適切な名前をつけたいのですが、なかなか思いつきません。
名前をつけにくいときには、以下の状況が考えられます。
- 一行で多くのことをやりすぎている
- そもそもまわりのコードがおかしい
順に見ていきます。
一行で多くのことをやりすぎている
a << v.split(":")[0].to_i
この一行では
- 文字列を
:
で区切り、 - その
0
番目をとりだし、 - 整数に変換し、
- 配列
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.each
やArray.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_i
とInteger#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
メソッドはその内部でbox
を15
に変換します。
見事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を題材に、いいコードについて考えました。
コードにはたくさんの評価軸があり、互いに複雑にからまりあっています。
評価軸ごとに最善のコードを考え、さらに評価軸同士のバランスをとりましょう。
また、いいコードは個別の状況に応じて変わるものです。
コードに何が求められているのかを判断できるようになりましょう。
この記事をきっかけに、コードを見る目がかわったら嬉しいです!