はじめに
この記事は「Ruby初心者向けのプログラミング問題を集めてみた」の電話帳問題を解く過程から、先輩のレビューをいただき、振り返るところまでを纏めた記事です。
備忘録・振り返り的な要素が強い為、フランクな書き方をしておりますが、大目に見ていただければと思います。軽い読み物だと思って読んでください。
また解き方に稚拙な箇所もあるかと思いますがご容赦ください。指摘等は大歓迎です。励みになります。
登場人物
- 私
- 社会人2年目PG。Rubyでコーディングし始めて1年とちょっと。うっかりポンコツ。最近Ruby Goldを取得した。
- ラテ太郎(アイコン参照)
- 私の心の中に住んでいる妖精。
白黒つけないいいやつ。ゆるい見掛けによらずしっかりしている。最後の砦。
- 私の心の中に住んでいる妖精。
- タピオカ先輩
- ラテ太郎の先輩妖精。コーディングが得意。
フェーズ1 「考える・自力で解く」
実際に出題された問題
# NameIndex
## 問題
- カタカナ文字列の配列を渡すと、ア段の音別にグループ分けした配列を返すプログラムを作成せよ。
- 各要素は 50 音順にソートもすること。
## 例
- IN: `['キシモト', 'イトウ', 'ババ', 'カネダ', 'ワダ', 'ハマダ']`
- OUT: `[ ['ア', ['イトウ']], ['カ', ['カネダ', 'キシモト']], ['ハ', ['ハマダ', 'ババ']], ['ワ', ['ワダ']] ]`
## 提出時、以下全ての条件を満たしていること
- `RSpec` でエラーが発生していないこと
- `RuboCop` で警告が出ていないこと
- `Coverage` が `100%` であること
考えたこと
私「なるほどなるほど、 辞書みたい
な感じでそれぞれの名前を纏めてあげればいいんだ。 out
の形ってなんだか group_by
した時の形に似てない?(※この時の私は空前の group_by
ブーム) 同じ感じ
でやれたらいいのにな〜。」
ラテ「こんなイメージで合ってるかな?」
class NameIndex
def self.create_index(names)
names.group_by { |name| name.chr }
end
end
私「そうそう、そんな感じ。私は group_by
のところを group_by(&:chr)
って書くかな。(※書かないとRuboCopに怒られる。)」
class NameIndex
def self.create_index(names)
names.group_by(&:chr)
end
end
names = ['キシモト', 'イトウ', 'ババ', 'カネダ', 'ワダ', 'ハマダ']
NameIndex.create_index(names)
=>{"キ"=>["キシモト"], "イ"=>["イトウ"], "バ"=>["ババ"], "カ"=>["カネダ"], "ワ"=>["ワダ"], "ハ"=>["ハマダ"]}
私「これを to_a
で配列にしたらイメージに近くない?なんかイメージに近い気がする!……でもこれって並び替えも必要だよね。うーん、 各要素はソートすること
って書いてあったよなぁ。各要素をソートする……。」
※シンキングタイム
私「最初から配列の中身ソートしとけばええやんけ…………………………………………………………………………。」
ラテ「気付くのおっそ…………。」
class NameIndex
def self.create_index(names)
names.sort.group_by(&:chr).to_a
end
end
names = ['キシモト', 'イトウ', 'ババ', 'カネダ', 'ワダ', 'ハマダ']
NameIndex.create_index(names)
=>[["イ", ["イトウ"]], ["カ", ["カネダ"]], ["キ", ["キシモト"]], ["ハ", ["ハマダ"]], ["バ", ["ババ"]], ["ワ", ["ワダ"]]]
私「これです!!!!!!!!!!!!」
私「ちょっとどうしよう凄いしっくりきた。」
私「初めて一行で書けたのでは?」
私「やった〜〜〜〜〜〜〜〜〜〜〜嬉しい!」
私「RSpec書いて、時間を置いてから見直して、リファクタリングできるところあったらリファクタリングしよ!」
・
・
・
・
・
・
・
・
ラテ「お判りいただけただろうか…………。」
ラテ「こいつは凄い愚かなヤツです。どの辺が愚かかというと、 自分もやればできるじゃん
という気持ちに慢心しているところが愚かなのです。見直しは重要だということは小学生でも知っているのです。勿論、こいつにもそういう気持ちはあったのだと思うのです。あるからこそ 時間を置いてから見直して
なんて言ってるのです。けれど、時間を置きすぎてはならないのです。なぜって時間には限りがあるから…………。」
・
・
・
・
・
・
・
・
・
・
・
・
私「待って?」
私「これ、 ア
とか ハ
で纏まってなくない?」
私「 辞書みたいなイメージ
が先行してたけどこれ 問題と違う
よね?」
私「(大混乱)」
私「落ち着け、落ち着け私。きっと ここまでの課程で考えてきたこと
と今までの経験が私を助けてくれるはず。」
私「グループ化するっていう発送は悪くないはず。 キーがインデックス、valueがそのインデックスに含まれる文字、みたいなハッシュ
を作ったらいいんじゃないか?マッピング的な……ちょっと違う気がするけど……。」
私「あ〜お、って 範囲オブジェクト
でいける?数字とアルファベット以外でもいける?ええい、ままよ!食らえ!」
('ア'..'オ').to_a
=> ["ア", "ィ", "イ", "ゥ", "ウ", "ェ", "エ", "ォ", "オ"]
私「すっっっっっっっっっっっっご」
私「まじか……いやまあそうよな……できるよな……できるんか……凄いなRuby……。」
私「待てよ、カタカナだったらあれよな、 ヴ
とかあるよな。え、ヴァネッサとか出てくるかな電話帳。いやでもこれ苗字だけとか書いてないしな。友達にヴァネッサとかおるかもしれんし……ヴァネッサだけ行き場ないとか可哀想よな……。」
ラテ「そうして出来上がった定数がこれ。」
INDEX = {
'ア': ('ア'..'オ').to_a << 'ヴ',
'カ': ('カ'..'ゴ').to_a,
'サ': ('サ'..'ゾ').to_a,
'タ': ('タ'..'ド').to_a,
'ナ': ('ナ'..'ノ').to_a,
'ハ': ('ハ'..'ボ').to_a,
'マ': ('マ'..'モ').to_a,
'ヤ': ('ヤ'..'ヨ').to_a,
'ラ': ('ラ'..'ロ').to_a,
'ワ': ('ワ'..'ン').to_a
}.freeze
私「 ン
っている???」
私「いや、ンダホさんとかおるかもしれんし……」
私「入れよう」
私「問題はここからよな」
私「 ア
は ア
のグループ、ってグループ分けしたい。」
私「グループ分けしたいけどどうやってメンバー選出する……?」
※シンキングタイム
私「メンバー選出といえば select
やんけ…………。」
私「さっき作った範囲の中に頭文字が当てはまる子(文字列)を選出してあげたらいいんじゃん? include?
でそれはチェックできる……で、それをうまいこと配列にして……なんかやっと見えてきたぞ…………。」
class NameIndex
INDEX = {
'ア': ('ア'..'オ').to_a << 'ヴ',
'カ': ('カ'..'ゴ').to_a,
'サ': ('サ'..'ゾ').to_a,
'タ': ('タ'..'ド').to_a,
'ナ': ('ナ'..'ノ').to_a,
'ハ': ('ハ'..'ボ').to_a,
'マ': ('マ'..'モ').to_a,
'ヤ': ('ヤ'..'ヨ').to_a,
'ラ': ('ラ'..'ロ').to_a,
'ワ': ('ワ'..'ン').to_a
}.freeze
def self.create_index(names)
INDEX.map do |key, value|
index_names = names.select { |name| value.include?(name.chr) }
end
end
end
私「そうそう、これで最初に想定してたみたいに 要素をソート
して。 index_names
が空じゃなかったらインデックスをつけてあげればいけるんじゃないか。」
class NameIndex
INDEX = {
'ア': ('ア'..'オ').to_a << 'ヴ',
'カ': ('カ'..'ゴ').to_a,
'サ': ('サ'..'ゾ').to_a,
'タ': ('タ'..'ド').to_a,
'ナ': ('ナ'..'ノ').to_a,
'ハ': ('ハ'..'ボ').to_a,
'マ': ('マ'..'モ').to_a,
'ヤ': ('ヤ'..'ヨ').to_a,
'ラ': ('ラ'..'ロ').to_a,
'ワ': ('ワ'..'ン').to_a
}.freeze
def self.create_index(names)
INDEX.map do |key, value|
index_names = names.select { |name| value.include?(name.chr) }.sort
[key.to_s, index_names] unless index_names.empty?
end
end
end
names = ['キシモト', 'イトウ', 'ババ', 'カネダ', 'ワダ', 'ハマダ']
NameIndex.create_index(names)
=>[["ア", ["アマネ"]], ["カ", ["キシモト"]], nil, nil, nil, ["ハ", ["ハマダ", "ババ"]], nil, nil, nil, ["ワ", ["ワダ"]]]
私「盲点」
私「うっそやん………そりゃそうだわ…………なんで気付かんのじゃ……。」
私「 compact
しよ………。」
私「ごちゃごちゃしてるし、 names
が空だったら素直に早期リターンしよ」
ラテ「そして完成したのがこれ。」
class NameIndex
INDEX = {
'ア': ('ア'..'オ').to_a << 'ヴ',
'カ': ('カ'..'ゴ').to_a,
'サ': ('サ'..'ゾ').to_a,
'タ': ('タ'..'ド').to_a,
'ナ': ('ナ'..'ノ').to_a,
'ハ': ('ハ'..'ボ').to_a,
'マ': ('マ'..'モ').to_a,
'ヤ': ('ヤ'..'ヨ').to_a,
'ラ': ('ラ'..'ロ').to_a,
'ワ': ('ワ'..'ン').to_a
}.freeze
def self.create_index(names)
return [] if names.empty?
INDEX.map do |key, value|
index_names = names.select { |name| value.include?(name.chr) }.sort
[key.to_s, index_names] unless index_names.empty?
end.compact
end
end
私「あっ、あ……RSpecも書き直しやん…………。先輩ごめんなさい………。(15分オーバー)」
フェーズ1で学んだこと
- カタカナも範囲オブジェクトにできる。
-
select
を使えばグループ化は簡単 - 慢心しない、簡単にできたと思った時ほど見直しはしっかり。
- 時間に余裕を持って作業する。
- 問題は定期的に読み直す。自分の認識が間違っていないか確認する。
- 方向性を見失わないようにテストファーストで作業をする。
フェーズ2 タピオカ先輩からレビューをもらう
この課題を解いた後、見守っていたタピオカ先輩からレビューをもらった。
タピ「レビューしたんだけどさ。」
私「はい。」
タピ「今回悪いところなかった。」
私「???」
私「またまたぁ」
タピ「いや本当。今回の評価ポイント(要件を満たせているか・可読性・テストコードの充実度)は満たせてる。」
私「(動揺)(困惑)」
タピ「とりあえずレビューしていこうか」
ポイント1 injectで無駄なく回す
タピ「私さん、 map
compact
してたじゃん。」
私「はい。map
だとnil
が混在しちゃうので」
タピ「じゃあ、nil
が混ざらないようにループ回したらよかったんじゃない?」
私「アッ」
-
map compact
orselect map
で作れる配列はinject
で作れ -
inject
で作れるならeach_with_object
にしなさい(RuboCopの好み)
タピ「この記事が参考になる。」
タピ「今回の問題だと、nil
になるものののチェックで無駄に処理が走るよね?例えば、電話帳にはア行しか登録されてないのに、他の行についての処理も走る。ループでいうなら2回ループが走ってる。必要なものだけの配列を作りたいなら inject
を使えば1回で済む。」
私「そっか……そっか……そうですね……(知識としては持っていたのに未だ使いこなせていないことに対する悔しさに襲われる)」
ポイント2 冗長にならないようなコーディングをする
タピ「あとさ、定数のハッシュ。キーと配列の0番目の値が一緒なの、なんか冗長じゃない?」
私「確かに……。」
タピ「しかもハッシュのキー、to_s
してるじゃん、to_s
するくらいならロケット記法
で最初からキーを文字列にしておけばよかったんじゃない?」
私「確かに……。」
タピ「しかもこのキーって配列作るときに使ってるだけじゃん?」
私「そうですね。そのためだけに用意しちゃいました。そのキーでまとめなきゃ!!って気持ちが強くて、キーを。」
タピ「つまりこう定義したら万事解決だったってこと。」
class NameIndex
SYLLABARIES = [
('ア'..'オ').to_a << 'ヴ',
('カ'..'ゴ').to_a,
('サ'..'ゾ').to_a,
('タ'..'ド').to_a,
('ナ'..'ノ').to_a,
('ハ'..'ボ').to_a,
('マ'..'モ').to_a,
('ヤ'..'ヨ').to_a,
('ラ'..'ロ').to_a,
('ワ'..'ン').to_a
].freeze
class << self
def create_index(names)
names.sort.group_by(&method(:initial)).to_a
end
private
def initial(name)
SYLLABARIES.find { |values| values.include?(name[0]) }.first
end
end
end
私「これです」
私「こういうの書きたかったんです」
私「group_by
使われてるし、メインのメソッドは一行で書かれてるし、超カッケー(配列か〜〜〜〜〜!!マッピングしなきゃって気持ちが強すぎた)」
〜タピオカ先輩の解説タイム〜
タピ「例えば、ア〜オ+ヴの中に、名字の1文字目が含まれていたらア行に纏められるわけでしょ。group_by
の中でアカサタナ〜が返れば今回の想定した配列が作れるじゃん。つまり、
『名字の1文字目がSYLLABARIES
の配列のどれかに含まれていたら、その配列の1文字目が返ればいい』
ってことね。それがinitial
メソッド。」
タピ「そのメソッドをgroup_by
と合わせて実行すればいいだけ。今回は &method
を使って書いてみた。」
私「(group_by
とはこうやって使うのか)」
タピ「どう?納得?」
私「納得しかないです……すげ……」
フェーズ2で学んだこと
-
group_by
はブロック内の返り値でグルーピングされる=>自分の思う返り値が返るようなメソッドを作って呼んであげればグルーピングはできる(今回はgroup_by
が使えた!) -
&method
を使うと短く書ける(参考: &演算子と、procと、Object#method について理解しなおす) - 冗長だと感じるところは徹底的に排除する(同じ文字がなんども出てくる、等。今回は定数の中身が冗長だった。)
最後に
やり方、考え方の方向性は合っていましたが、テクニカルな部分がまだまだだなと実感しました。しかしこれで group_by
はマスターです。&method
はあまり使ったことがなかったので、使える場面では使っていきたいと思います。
また、自分の書くコードは冗長になりがちなので、「これって冗長じゃない?」という気持ちをもってコーディングすることを意識する必要があるなと感じました。
次の回ではもっといいコードを書けるように腕を磨いておきます。