Effective Rubyを読んだので感想を書いてく
はじめに
Effective Ruby、やっと読み終わった!!!
ゴールデンウィーク中の課題図書だったのに、思ったよりも内容が重くて読み終わるのに一ヶ月くらいかかってしまった。
- 作者: Peter J.Jones
- 出版社/メーカー: 翔泳社
- 発売日: 2015/01/19
- メディア: Kindle版
- この商品を含むブログ (5件) を見る
本書は48項目から構成されていてるんですが、このエントリは、うちいくつかの項目についての感想や備忘録になります。
なお、各項目におけるサンプルコードは本書に掲載されているものではなく、復習のために自分で書いたものです。
感想・備忘録など
項目4 定数がミュータブルなことに注意しよう
定数が配列やハッシュなどのコレクションオブジェクトの場合は、コレクションの要素もfreeze
しないとダメ。
# ダメな例 PERFUME = %w(Aa-CHAN NOCCHi KASHIYUKA).freeze PERFUME.map(&:downcase!) #=> ["aa-chan", "nocchi", "kashiyuka"] # 要素に対して破壊的メソッドを実行できてしまう # 良い例 PERFUME = %w(Aa-CHAN NOCCHi KASHIYUKA).map!(&:freeze).freeze PERFUME.map(&:downcase!) #=> RuntimeError: can't modify frozen String
さらに、定数へ値が再代入されるのも防ぎたい場合には(意図しない再代入が行われる例が思いつかないのだけど)、定数をクラスやモジュール内で定義して、これをfreeze
する。
module Defaults TIMEOUT = 3 end Defaults.freeze Defaults::TIMEOUT = 46 #=> RuntimeError: can't modify frozen Module
項目10 構造化データの表現にはHashではなくStructを使おう
Hash
の保持する値のいくつかを組み合わせて演算を行っている部分があったら、Struct
にしてしまった方がよりオブジェクト指向らしい感じになっていいよね、という話。
require 'csv' # members.csv # id,first_name,family_name # 28,奈々未,橋本 # 6,万理華,伊藤 # 手数は少ないがイマイチな例 data = CSV.table('members.csv') data.map { |row| row[:family_name] + row[:first_name] } #=> ["橋本奈々未", "伊藤万理華"] # Structを利用する例 Member = Struct.new(:id, :first_name, :family_name) do # Struct::newのブロック内でインスタンスメソッドを定義できる def full_name family_name + first_name end end members = CSV.table('members.csv').map { |row| Member.new(*row.fields) } members.map(&:full_name) #=> ["橋本奈々未", "伊藤万理華"]
わりとありそうなケースだと思うので覚えておきたい。
項目17 nil、スカラーオブジェクトを配列に変換するには、Arrayメソッドを使おう
オブジェクトをArray
メソッドでラップするといい感じに配列に変換できるので、メソッドの引数とかに対して使うと便利。
Array(755) # ほとんどのオブジェクトでは要素が1つの配列が返る #=> [755] Array(%w(こしじまとしこ 中田ヤスタカ)) # 配列はそのまま #=> ["こしじまとしこ", "中田ヤスタカ"] Array(nil) # nilを渡すと空の配列になる(これが便利!) #=> []
項目19 reduceを使ってコレクションを畳み込む方法を身に付けよう
reduce
メソッドって数値配列の合計とかを算出するときには使おうと思えるんだけど、畳み込んだあとの結果がHash
ときにも使えるのをよく忘れてしまう。
# よくやってしまう書き方 def to_h hash = {} attrs.each do |name| hash[name] = public_send(name) end hash end # reduceを使うと綺麗に書ける def to_h attrs.reduce({}) do |hash, name| hash.merge!(name => public_send(name)) end end
項目20 ハッシュのデフォルト値を利用することを検討しよう
Hash
はnil
以外のデフォルト値を設定できるので、キーが存在するかどうか確認したいときはhas_key?
を使おうね、という話がよかった。
# ダメな例 # キーが存在するどうか調べるために、デフォルト値がnilであることを利用している if hash[key] # do something end # 正しくはこう # コードでやりたいことが表現されており、デフォルト値がnil以外でも動く if hash.has_key?(key) # do something end
この話に限らずなんだけど、なにかを実装しようとしたときにそれがコードで表現できていない(AのBという性質を利用してCという目的を達成する、みたいなやつ)というのはクソコードにありがちなパターン*1なので、気をつけていきたい。
項目30 method_missingではなくdefine_methodを使うようにしよう
method_missing
はpublic_methods
やrespond_to?
に正しく応答することができないので、まずはdefine_method
を使うことを考えよう、という話。
# method_missingでの実装例 require 'romaji' class Group def method_missing(name, *args, &block) if name == :yell group_name = Romaji.romaji2kana(self.class.name, kana_type: :hiragana) "うちらは #{group_name} のぼりざか 46!" else super end end end Kojizaka = Class.new(Group) kojizaka = Kojizaka.new # メソッドは期待通りの値を返すが、public_methodsやrespond_to?に正しく応答することができない kojizaka.yell #=> "うちらは こじざか のぼりざか 46!" kojizaka.public_methods.include?(:yell) #=> false kojizaka.respond_to?(:yell) #=> false
# define_methodでの実装例 require 'romaji' class Group define_method(:yell) do group_name = Romaji.romaji2kana(self.class.name, kana_type: :hiragana) "うちらは #{group_name} のぼりざか 46!" end end Nogizaka = Class.new(Group) nogizaka = Nogizaka.new # public_methodsやrespond_to?にも正しく応答できる nogizaka.yell #=> "うちらは のぎざか のぼりざか 46!" nogizaka.public_methods.include?(:yell) #=> true nogizaka.respond_to?(:yell) #=> true
項目34 Procの引数の個数の違いに対応できるようにすることを検討しよう
proc
とlambda
(本書では前者を弱いProc
オブジェクト、後者を強いProc
オブジェクトと呼んでいる)では引数の扱いに違いがあるらしい。知らなかった、、、
# proc(or Proc.new)は引数の扱いが超ユルい # 以下はlambdaだと全部エラーになる proc { |a, b| [a, b] }.call(1) #=> [1, nil] # 足りない引数にはnilが渡される proc { |a, b| [a, b] }.call(1, 2, 3) #=> [1, 2] # 余分な引数を渡してもエラーは起きず、単に無視される proc { |a, b| [a, b] }.call([1, 2]) #=> [1, 2] # 配列を渡すと親切に?展開してくれる
引数の扱いが厳密かどうかはProc#lambda?
メソッドで調べることができる。
def test(&block) block.lambda? end Proc.new {}.lambda? #=> false proc {}.lambda? #=> false lambda {}.lambda? #=> true method(:test).to_proc.lambda? #=> true test {} #=> false
また、Proc
インスタンスが受け付ける引数の個数を返すProc#arity
というメソッドがある。
このメソッドの挙動を理解するために、受け取ったブロックの必須引数の個数を返すメソッドを実装してみた。
def count_required_args(&block) return unless block # Proc.arityはレシーバが可変長引数を受け付ける場合に1の補数を返すので、~(単行補数演算子)を使う (arity = block.arity) > 0 ? arity : ~arity end
項目37 MiniTestスペックテストに慣れよう
MiniTest
のspecインターフェイスがもはやRSpec
だった。
# https://github.com/seattlerb/minitest より引用 require "minitest/autorun" describe Meme do before do @meme = Meme.new end describe "when asked about cheeseburgers" do it "must respond positively" do @meme.i_can_has_cheezburger?.must_equal "OHAI!" end end describe "when asked about blending possibilities" do it "won't say no" do @meme.will_it_blend?.wont_match /^no/i end end end
Specを書くための、言語によらないDSLがあればいいのになあと一瞬思った。
項目47 ループ内ではオブジェクトリテラルを避けよう
Ruby2.1以降では、フリーズされた文字列は定数と同じになるというのは初めて知った。
# freezeした文字列リテラルはプログラム全体で共有される ['iPhone'.freeze, 'iPhone'.freeze].map(&:object_id) #=> [70092805607340, 70092805607340] # 文字列リテラルをfreezeしないとダメ %w(Android Android).map(&:freeze).map(&:object_id) #=> [70092805500560, 70092805500540]
これをループ処理で利用することで、使い捨てのオブジェクトが大量に生成されるのを防げる。
# よく書くコード # '乃木坂46'がmembers.size個生成される members.all? { |member| member.group == '乃木坂46' } # 効率的なコード # '乃木坂46'は1個だけ生成される members.all? { |member| member.group == '乃木坂46'.freeze }
おわりに
このエントリで取り上げませんでしたが、本書ではRubyの挙動(継承階層とかGC)に関する話も載っています。
その分読むのが大変ですが、読み終わるとRubyistとしてのレベルが1個上がった気がします(笑)。
まだ読んでない方にはぜひオススメしたい本だと思いました。
*1:しかも本人は「やべえ俺めっちゃ頭いいわ」とか思っていることが往々にしてある