コードレビューをしているときに、ん?と思った挙動がありました。サンプルコードです。
class Hoge
  attr_accessor :fuga
  
  def piyo
    fuga = "overwrite" if fuga.nil?
    fuga
  end
end
hoge = Hoge.new
hoge.fuga = "fuga"
hoge.piyo
この時の hoge.piyo の結果はどうなると思いますか? まともなRubyistであれば、分かると思いますが僕にとっては予想外の結果でした。
僕はこのコードを最初に見たときにパッと思った答えは fuga が返ってくるんじゃないかと思いました。が、実際の答えは overwrite です。なぜでしょうか。その理由を探るためにRubyの仕様を確認してみると、とても面白かったのでまとめてみました。
(注) attr_accessor で定義したインスタンス変数にアクセスするのに @ がないのがそもそもオカシイというツッコミは一旦置いといてもらえると助かります 🙇
Rubyはレシーバが省略できる
まずRubyはレシーバが省略できます。下記のコードのように self.fuga と書かなくても現在のレシーバ self を参照してくれます。
class Hoge
  def piyo
    fuga
  end
  
  def fuga
   p "fuga"
  end
end
しかし、同名のローカル変数がある場合はそちらを優先します。メソッド内に fuga という変数がないかを確認し、なければ同名のメソッドを探しに行きます。
class Hoge
  def piyo
    fuga = "fuga"
    fuga #=> ローカル変数の fuga
  end
  
  def fuga
   p "fuga"
  end
end
このことは object_id を確認すると、よりわかります。
class Hoge
  def piyo
    p fuga.object_id #=> 8
    fuga = "fuga"
    p fuga.object_id #=> 70220665709200
  end
  
  def fuga
  end
end
Hoge.new.piyo
privateメソッドはレシーバを指定できない
privateメソッドの場合はレシーバを指定できません。つまり、常に省略した形の記法になります。
class Hoge
  def piyo
    fuga
  end
  
  private
  def fuga
    "private fuga"
  end
end
self.fuga とレシーバを明示して呼ぶと NoMethodError: private method ``fuga' called for #<Hoge:0x007f9f3c0538a8> エラーになります。これはよくよく考えると当然の話しでレシーバを指定できてしまうと他のオブジェクトから呼び出せることになってしまうからです。self に対してのみ呼び出せないと駄目なのです。
privateメソッドと同名のローカル変数
privateメソッドと同名のローカル変数を宣言した場合はどうなるかというと前述した通りローカル変数が優先されます。
class Hoge
  def piyo
    fuga = "public fuga"
    fuga
  end
  
  private
  def fuga
    "private fuga"
  end
end
これは "public fuga" が返って来ます。
ローカル変数の宣言
Rubyでは実行されていなくても宣言とみなされる仕様があります。以下のコードは一見 fuga がNameErrorになりそうなきがしますが、nil が返って来ます。
class Hoge
  def piyo
    if false
      fuga = "fuga"
    end
    
    fuga
  end
end
Hoge.new.piyo
# => nil
これは不思議に感じますが前述したレシーバを省略して書けるというのを考えると分かります。fuga と書いた場合、それがローカル変数なのかメソッド呼び出しなのかが分かりにくいですが、変数代入のコードが書いてあるとローカル変数を参照するというのが判断できるため、デフォルト値として nil を返す挙動になっています。実際に代入されなくても初期化式さえあればOKです。
Ruby は最初にスクリプト全体をコンパイルしてローカル変数を決定します。
https://docs.ruby-lang.org/ja/latest/library/irb.html
さすがに初期化子式が出てくる前に参照するとエラーになります。
class Hoge
  def piyo
    fuga #=> NameError: undefined local variable or method `fuga'
    
    if false
      fuga = "fuga"
    end
  end
end
ちなみに、インスタンス変数は @ が付いているため初期化式がなくてもデフォルトの nil が返って来ます。
def hoge
  @piyo
end
hoge
# => nil
attr_accessorの実態
attr_accessor は単純にゲッターセッターメソッドを作っているだけです。以下の2つは同じことです。
class Hoge
  attr_accessor :fuga
end
class Hoge
  def fuga
    @fuga
  end
  
  def fuga=(fuga)
    @fuga = fuga
  end
end
ちなみに def fuga=(fuga) というのがちょっと見慣れないメソッド定義の書き方だと僕は思いました。が、単純に属性への代入を、変数への代入と同じように書けるようにするシンタックスシュガーということでした。
attr_accessorをメタプロで
実際の attr_accessor はCで実装されていますが、それをメタプロで表現しようとすると以下のような感じになります。こんな事ができるRubyはカッコいいですね。
def accessor(sym)
  setter(sym)
  getter(sym)
end
def setter(sym)
  define_method("#{sym}=") do |value|
    instance_variable_set("@#{sym}", value)
  end
end
def getter(sym)
  define_method(sym) do
    instance_variable_get("@#{sym}")
  end
end
class Hoge
  accessor :piyo
end
hoge = Hoge.new
hoge.piyo = "piyo"
hoge.piyo #=> piyo
Class内に任意の式を書ける
前述の attr_accessor をメタプロで書いた際に Hogeクラス の直下にメソッド呼び出しをしていますが、これがまたRubyらしい面白いところで、Rubyではクラス定義の中では任意の式が書けます。これがあることでメタプロがよりメタプロっぽく感じますね。
class Hoge
  p "This is Hoge class"
  10.times {|i| p i }
end
まとめ
色々と寄り道しましたが、上記のことを簡単にまとめると以下の2点になります。
- レシーバ省略した同名のローカル変数とメソッド呼び出しは、ローカル変数が優先される
 - ローカル変数の初期化は、初期化式さえあればデフォルトの
nilで宣言される 
これらの仕様を照らし合わせると最初のコードがエラーになるのがわかります。
class Hoge
  attr_accessor :fuga
  
  def piyo
    fuga = "overwrite" if fuga.nil?
    fuga
  end
end
hoge = Hoge.new
hoge.fuga = "fuga"
hoge.piyo
僕が考えた処理の流れは以下のとおりです。間違っている可能性があるので、ツッコミをお待ちしております 🙇
- 
fuga = "overwrite"があるな… - 代入メソッドの 
def fuga=(fuga)があるけど、ローカル変数の方が優先度高いな - 
fuga = "overwrite"が初期化式だからfugaをnilで初期化しよう - 
nil.nil?はtrueだからfugaにoverwrite代入しよう 
hoge オブジェクトの中身を見てみると fuga がインスタンス変数ではなくローカル変数を参照しているのがわかります。
hoge
# => #<Hoge:0x007fc0511a5db0 @fuga="fuga">
正しい書き方の前に、以下のコードでは何が返ってくるか考えてみると、より分かります。違いは単純にifの条件が false 固定になっていることです。
class Hoge
  attr_accessor :fuga
  
  def piyo
    fuga = "overwrite" if false
    fuga
  end
end
hoge = Hoge.new
hoge.fuga = "fuga"
hoge.piyo
hoge.piyo の返り値は nil になります。流れは同じですね。
ではどのように書くのが正しいのかというと以下のようにインスタンス変数の参照としてちゃんと @ を付けるという超基本的な事になります。
class Hoge
  attr_accessor :fuga
  
  def piyo
    @fuga = "overwrite" if @fuga.nil?
    @fuga
  end
end
hoge = Hoge.new
hoge.fuga = "fuga"
hoge.piyo
めでたく fuga が返ってきました。ありがとうございます。Rubyって面白くて楽しいですね。