Ruby初心者が変数宣言のタイミング、レシーバを省略した時の挙動にハマったので仕様を確認したら面白かった


コードレビューをしているときに、ん？と思った挙動がありました。サンプルコードです。

```ruby
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` を参照してくれます。

```ruby
class Hoge
  def piyo
    fuga
  end
  
  def fuga
   p "fuga"
  end
end
```

しかし、同名のローカル変数がある場合はそちらを優先します。メソッド内に `fuga` という変数がないかを確認し、なければ同名のメソッドを探しに行きます。

```ruby
class Hoge
  def piyo
    fuga = "fuga"
    fuga #=> ローカル変数の fuga
  end
  
  def fuga
   p "fuga"
  end
end
```

このことは `object_id` を確認すると、よりわかります。

```ruby
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メソッドの場合はレシーバを指定できません。つまり、常に省略した形の記法になります。

```ruby
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メソッドと同名のローカル変数を宣言した場合はどうなるかというと前述した通りローカル変数が優先されます。

```ruby
class Hoge
  def piyo
    fuga = "public fuga"
    fuga
  end
  
  private
  def fuga
    "private fuga"
  end
end
```

これは `"public fuga"` が返って来ます。

## ローカル変数の宣言

Rubyでは実行されていなくても宣言とみなされる仕様があります。以下のコードは一見 `fuga` がNameErrorになりそうなきがしますが、`nil` が返って来ます。

```ruby
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

さすがに初期化子式が出てくる前に参照するとエラーになります。

```ruby
class Hoge
  def piyo
    fuga #=> NameError: undefined local variable or method `fuga'
    
    if false
      fuga = "fuga"
    end
  end
end
```

ちなみに、インスタンス変数は `@` が付いているため初期化式がなくてもデフォルトの `nil` が返って来ます。

```ruby
def hoge
  @piyo
end

hoge
#=> nil
```

## attr_accessorの実態

`attr_accessor` は単純にゲッターセッターメソッドを作っているだけです。以下の2つは同じことです。

```ruby
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はカッコいいですね。

```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ではクラス定義の中では任意の式が書けます。これがあることでメタプロがよりメタプロっぽく感じますね。

```ruby
class Hoge
  p "This is Hoge class"
  10.times {|i| p i }
end
```

## まとめ

色々と寄り道しましたが、上記のことを簡単にまとめると以下の2点になります。

- レシーバ省略した同名のローカル変数とメソッド呼び出しは、ローカル変数が優先される
- ローカル変数の初期化は、初期化式さえあればデフォルトの`nil`で宣言される

これらの仕様を照らし合わせると最初のコードがエラーになるのがわかります。

```ruby
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` がインスタンス変数ではなくローカル変数を参照しているのがわかります。

```ruby
hoge
#=> #<Hoge:0x007fc0511a5db0 @fuga="fuga">
```

正しい書き方の前に、以下のコードでは何が返ってくるか考えてみると、より分かります。違いは単純にifの条件が `false` 固定になっていることです。

```ruby
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` になります。流れは同じですね。

ではどのように書くのが正しいのかというと以下のようにインスタンス変数の参照としてちゃんと `@` を付けるという超基本的な事になります。

```ruby
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って面白くて楽しいですね。