Effective Rubyを読んだので感想を書いてく

はじめに

Effective Ruby、やっと読み終わった!!!
ゴールデンウィーク中の課題図書だったのに、思ったよりも内容が重くて読み終わるのに一ヶ月くらいかかってしまった。

Effective Ruby

Effective Ruby

本書は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 ハッシュのデフォルト値を利用することを検討しよう

Hashnil以外のデフォルト値を設定できるので、キーが存在するかどうか確認したいときは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_missingpublic_methodsrespond_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の引数の個数の違いに対応できるようにすることを検討しよう

proclambda(本書では前者を弱い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:しかも本人は「やべえ俺めっちゃ頭いいわ」とか思っていることが往々にしてある