Code Fix

上級

coerce未実装のクラスと数値を演算すると TypeError になる

自作クラスのインスタンスを 数値 + 自作オブジェクト の順で演算すると、Integer側の+が呼ばれます。Integerは自作クラスの値をどう数値として扱えばよいか分からないため、そのクラスにcoerceメソッドが定義されていない限りTypeErrorになります。

エラーメッセージの読み方

main.rb:11:in `+': Money can't be coerced into Integer (TypeError)
main.rb
ファイル名
11
行番号 — 実際にクラッシュした行
+
発生場所 — <main> ならトップレベル、メソッド名ならそのメソッドの中
TypeError
例外クラス — 何が起きたか。ここを検索するのが最短です
Money can't be coerced into Integer
詳細メッセージ — どの値が問題だったか

このエラーが出る典型パターン

パターン1

 1  class Money
 2    attr_reader :amount
 3    def initialize(amount)
 4      @amount = amount
 5    end
 6    def +(other)
 7      Money.new(amount + other.amount)
 8    end
 9  end
10  
11  total = 5 + Money.new(50)           
                          ^
12  puts total.amount
main.rb:11:in `+': Money can't be coerced into Integer (TypeError) from main.rb:11:in `<main>'

数値を左に置いた演算はInteger側の+が呼ばれます。Moneyにcoerceを実装していない限り、数値がMoneyをどう扱えばよいか分かりません。

直し方: 5 + Money.new(50) を Money.new(50) + Money.new(5) にします。

この問題を解いてみる →

広告
広告スロット(未設定)

パターン2

 1  class Weight
 2    attr_reader :grams
 3    def initialize(grams)
 4      @grams = grams
 5    end
 6    def +(other)
 7      Weight.new(grams + other.grams)
 8    end
 9  end
10  
11  total = 5 + Weight.new(50)            
                           ^
12  puts total.grams
main.rb:11:in `+': Weight can't be coerced into Integer (TypeError) from main.rb:11:in `<main>'

自作クラスに+を定義しても、それが呼ばれるのはレシーバが自作クラス側にある場合だけです。

直し方: 5 + Weight.new(50) を Weight.new(50) + Weight.new(5) にします。

この問題を解いてみる →

パターン3

 1  class Length
 2    attr_reader :cm
 3    def initialize(cm)
 4      @cm = cm
 5    end
 6    def +(other)
 7      Length.new(cm + other.cm)
 8    end
 9  end
10  
11  total = 5 + Length.new(50)            
                           ^
12  puts total.cm
main.rb:11:in `+': Length can't be coerced into Integer (TypeError) from main.rb:11:in `<main>'

数値を左に置いた式でも自作クラスの計算を成立させたい場合は、coerceメソッドの実装が必要です。

直し方: 5 + Length.new(50) を Length.new(50) + Length.new(5) にします。

この問題を解いてみる →

パターン4

 1  class Volume
 2    attr_reader :ml
 3    def initialize(ml)
 4      @ml = ml
 5    end
 6    def +(other)
 7      Volume.new(ml + other.ml)
 8    end
 9  end
10  
11  total = 5 + Volume.new(50)            
                           ^
12  puts total.ml
main.rb:11:in `+': Volume can't be coerced into Integer (TypeError) from main.rb:11:in `<main>'

演算の左側のオブジェクトが優先されるというRubyの規則を知らないと、この順序依存のエラーは謎に見えます。

直し方: 5 + Volume.new(50) を Volume.new(50) + Volume.new(5) にします。

この問題を解いてみる →

パターン5

 1  class Score
 2    attr_reader :points
 3    def initialize(points)
 4      @points = points
 5    end
 6    def +(other)
 7      Score.new(points + other.points)
 8    end
 9  end
10  
11  total = 5 + Score.new(50)           
                          ^
12  puts total.points
main.rb:11:in `+': Score can't be coerced into Integer (TypeError) from main.rb:11:in `<main>'

自作クラスを数値と組み合わせる演算は、どちらを左に置くかで呼ばれるメソッドが変わることを意識する必要があります。

直し方: 5 + Score.new(50) を Score.new(50) + Score.new(5) にします。

この問題を解いてみる →

よくある誤解

「自作クラスに+を定義しておけば、数値を左に置いた演算でもそれが呼ばれるはず」という思い込みは誤りです。演算子は左側のオブジェクトのメソッドが優先されるため、数値を左に置いた場合は自作クラス側ではなくInteger側の+が呼ばれ、coerceによる変換手順を用意しない限り失敗します。

実務での勘所

coerceが解決の鍵になるのは、Integerの+が、認識できない相手を渡された最後の手段として、その相手のcoerceメソッドを呼び出す設計になっているためです。自作クラスにdef coerce(other); [MyClass.new(other), self]; endのようなメソッドを実装しておくと、Integer側はこれを呼び、返ってきた「両方とも比較可能な形にそろえた2つの値」を使って、あらためて自作クラス同士の+として計算をやり直します。coerceは「私と、あなたを、両方とも同じ土俵で扱えるペアに変換してください」という依頼に応える窓口です。

演習をはじめる

関連するエラー

広告
広告スロット(未設定)