Ruby: OpenStruct のキーは to_sym される

# はじめに
Ruby の標準添付ライブラリに OpenStruct があります。

- [class OpenStruct (Ruby 2.1.0 リファレンスマニュアル)](http://docs.ruby-lang.org/ja/2.1.0/class/OpenStruct.html)

組み込みクラス Struct (扱い方がC言語の構造体に近い感じ)は使いにくい場面が多いので、私は OpenStruct をその代わりにすることが多いです。

## OpenStruct の特徴
特徴は「attr on write」(書き込んだ時に属性が定義される； もともと Python にあった機能らしい)です。 
Ruby のオブジェクトには外部から直接アクセスできる変数/属性(他言語でいう public 可視性の変数/属性)といったものはありませんが、値を書き込んだ時に setter/getter (アクセサメソッド)が作られるイメージです。

```rb:sample1.rb
require 'ostruct'

st1 = OpenStruct.new
st1.foo = 'hello'           # この時 foo/foo= が作られる
st1.bar = 10                # この時 bar/bar= が作られる

p st1.foo   #=> "hello"     # 値を取得
p st1.bar   #=> 10          # 値を取得
p st1.baz   #=> nil         # 定義されてないものは nil
```
```rb:sample2.rb
require 'ostruct'
                            # new の引数にハッシュを渡し、値を設定することもできる
st2 = OpenStruct.new(foo: 'hello', bar: 10)

p st2.foo   #=> "hello"     # 値を取得
p st2.bar   #=> 10          # 値を取得
```

# `[]`,`[]=`によるアクセス
上では setter/getter で値にアクセスしましたが、Hash のように `[]`,`[]=`でも値にアクセスできます。
扱い方の雰囲気は、JavaScript のオブジェクトのプロパティに似ていると思います。

```rb:sample3.rb
require 'ostruct'

st3 = OpenStruct.new

st3[:foo] = 'hello'           # `OpenStruct#[]=` で値を設定 
p st3[:foo]   #=> "hello"     # `OpenStruct#[]` で値を取得
p st3.foo     #=> "hello"     # getter で値を取得

st3.bar = 10                  # setter で値を設定
p st3[:bar]   #=> 10          # `OpenStruct#[]` メソッドで値を取得
p st3.bar     #=> 10          # getter で値を取得
```

# `[]`,`[]=`のキー
ところで、Hash のキーには Symbol を使うことが多いと思いますが、Symbol 以外も使えます。

```rb:sample4.rb
hash = {}
hash[:key]  = 'hello'        # キーは Symbol
hash['key'] = 'goodbye'      # キーは String 

p hash[:key]    #=> "hello"
p hash['key']   #=> "goodbye"
```

Hash のキーは `eql?` メソッドで同じか否かの判定を行います。
なので、`:key` と `'key'` は区別されます。

```rb
p :key.eql? 'key'   #=> false
```

これに対し、OpenStruct はキーを `to_sym` します。
(あるいは、キーとして与えられたオブジェクトを `to_sym` したもの(Symbol)をキーとして扱う)

```rb:sample5.rb
require 'ostruct'

p 'key'.to_sym         #=> :key      # 'key' を to_sym すると :key

st5 = OpenStruct.new
st5[:key] = 'hello'
st5[:key]               #=> "hello"
st5['key']              #=> "hello"

st5['key'] = 'goodbye'
st5[:key]               #=> "goodbye"
st5['key']              #=> "goodbye"

st5.foo = 100
st5[:foo]               #=> 100
st5['foo']              #=> 100

                  # 適当なオブジェクトでも to_sym で :foo を返すようにすれば...
obj = Object.new
def obj.to_sym          
  :foo 
end
p obj.to_sym           #=> :foo

                  # ... :foo の値にアクセスできるようになる
st5[obj]               #=> 100    
```

OpenStruct では to_sym できないオブジェクトをキーにすると例外が発生します。

```rb:sample6.rb
require 'ostruct'

hash = {}
hash[1] = 100
hash[1]            #=> 100

st6 = OpenStruct.new
st6[1] = 100      #!!  NoMethodError: undefined method `to_sym' for 1:Fixnum
```

# `ARGV.getopts` や YAML と一緒に使ってみる
`:key` と `'key'` をキーとして同一視するなど OpenStruct にはクセがあります。
ですが、私は`ARGV.getopts` や YAML (or JSON)と一緒に使ったりします。

`ARGV.getopts` も YAML も、キーが文字列なので Symbol にならんかな、といったものです。
(YAML の場合は、書き方次第でSymbolをキーにできますが、私は可搬性に抵触するなどの理由で結局文字列をキーにすることが多いです)

以下は例です。

```rb:sample7_before.rb
opts = ARGV.getopts 'abc:'

p opts['a']
p opts['b']
    :
```

OpenStruct を使った場合

```rb:sample7_after.rb
require 'ostruct'

opts = OpenStruct.new(ARGV.getopts 'abc:')

p opts.a            # opts[:a] でも同じ
p opts.b            # opts[:b] でも同じ
   :
```

YAML 読み込みの例です。

```yaml:sample.yml
---
# YAML サンプル
foo: hello, world
bar: have fun
```
```rb:sample8_before.rb
require 'yaml'

hash = YAML.load open 'sample.yml'
p hash['foo']     #=> "hello, world"
p hash['bar']     #=> "have fun"
```

OpenStruct を使った場合

```rb:sample8_after.rb
require 'yaml'
require 'ostruct'

st8 = OpenStruct.new(YAML.load open 'sample.yml')
p st8.foo     #=> "hello, world"       # p st[:foo] でも同じ
p st8.bar     #=> "have fun"           # p st[:bar] でも同じ
```

文字列をキーに`[]`でアクセスするのが、私には居心地が悪いと感じるので、それが OpenStruct で解消されます。
ただし、Hash と異なり OpenStruct は `each` や `keys` などが使えないのでケースバイケースで使い分ける必要があります。
(※2015/01/05 追記) OpenStruct には `each_pair` はあります。コメント欄を参照ください。 

# おわりに
本稿の動作確認は以下の環境で行っています。

- Ruby 2.1.5 p273
- Ubuntu Linux 14.04

# ちょっと、追記
OpenStruct では to_sym するものをキーにするので文字列をキーにできます。
空文字列(`''`) や 空白を含む文字列(`'a b'`) や数字だけの文字列(`'1'`) もキーにできます。

```rb:sample9.rb
require 'ostruct'

st9 = OpenStruct.new('' => :foo, 'a b' => :bar, '1' => :baz)
p st9['']       #=> :foo
p st9['a b']    #=> :bar
p st9['1']      #=> :baz 
```

この時 setter/getter はどう呼び出すのでしょう？
無論、まっとうな方法では Syntax Error になり呼び出せません。

ただし、send メソッドを使う、method メソッドで Method オブジェクトとして取り出して call する、等の方法で呼び出すことはできます。

```rb:sample9_2.rb
# ...上の続き (getter の場合)

st9.send ''              #=> :foo
st9.send 'a b'           #=> :bar
st9.send '1'             #=> :baz

st9.method('').()        #=> :foo    # st9.method('').call    と書いても同じ
st9.method('a b').()     #=> :bar    # st9.method('a b').call と書いても同じ
st9.method('1').()       #=> :baz    # st9.method('1').call   と書いても同じ
```
```rb:sample9_3.rb
# ...上の続き (setter の場合)

st9.send '=', 10
st9.send 'a b=', 20
st9.send '1=', 30

p st9['']        #=> 10
p st9['a b']     #=> 20
p st9['1']       #=> 30

st9.method('=').(40)                # st9.method('=').call(40)    と書いても同じ
st9.method('a b=').(50)             # st9.method('a b=').call(50) と書いても同じ
st9.method('1=').(60)               # st9.method('1=').call(60)   と書いても同じ

p st9['']        #=> 40
p st9['a b']     #=> 50
p st9['1']       #=> 60
```

わざわざこのようなことをすることも無いと思いますが、YAML 内容から OpenStruct オブジェクトを作った時など、(setter/getterの)メソッド名にならないかのようなキーがあったとしても、上のように何とかなります。