【Ruby 3.0 Advent Calendar 2020】OpenStruct で既存のメソッドを呼び出せるようになった【8日目】

Ruby 3.0 Advent Calendar 2020 8日目の記事になります。
昨日は Rubyで型チェック!動かして理解するRBS入門 〜サンプルコードでわかる!Ruby 3.0の主な新機能と変更点 Part 1〜 です。
今日は OpenStruct について書きます。

OpenStruct について

OpenStruct を使うと簡単な構造体を定義することができます。

require "ostruct"

homu = OpenStruct.new(name: "homu", age: 14)

# .new に渡したプロパティにアクセスできる
pp homu.name   # => "homu"
pp homu.age    # => 14

# 新しい要素を追加することもできる
homu.id = 1
pp homu.id     # => 1

こんな感じで任意のデータ構造を定義して値を参照したり追加したりすることができます。

Object クラスにメソッドが追加されると互換性の問題があった

この OpenStruct なんですが Ruby 2.7 では Object などで定義されているメソッド名と同じ名前だと値を参照する事ができません。

require "ostruct"
# 各バージョン
pp RUBY_VERSION          # => "2.7.2"
pp OpenStruct::VERSION   # => "0.2.0"

# Object#class や Object#hash と同じ名前で定義する
obj = OpenStruct.new(class: "class", hash: "hash")

# .new で定義した値は返ってこない…
p obj.class   # => OpenStruct
p obj.hash    # => 4198709777848673391

これは互換性の問題があり、例えば Ruby 2.5 で then という名前で要素を定義していたとします。

require "ostruct"

pp RUBY_VERSION          # => "2.5.6"

# then という名前で定義する
obj = OpenStruct.new(then: "then")

# .new で定義した値が返ってくる
pp obj.then
# => "then"

Ruby 2.5 では then は有効な名前なのですが Ruby 2.6 で Object#then という新しいメソッドが追加されました。
なので先程のようなコードは Ruby 2.5 では意図する動作をしていましたが Ruby 2.6 では壊れてしまいます。

require "ostruct"

pp RUBY_VERSION          # => "2.6.6"

# then という名前で定義する
obj = OpenStruct.new(then: "then")

# Object#then が返ってくる…
pp obj.then
# => #<Enumerator: ...>

このように Object クラスに新しいメソッドを追加する場合に OpenStruct に対して互換性の問題がありました。

OpenStruct で既存のメソッド名で呼び出せるようになった

Ruby 3.0(正確にいうと最新の ostruct gem なのですが)からは Object のメソッドと同名でも定義できるようになりました。

require "ostruct"
# 各バージョン
pp RUBY_VERSION          # => "3.0.0"
pp OpenStruct::VERSION   # => "0.3.1"

# Object のメソッドと同名で定義できる
obj = OpenStruct.new(class: "class", hash: "hash", then: "then")

p obj.class   # => "class"
p obj.hash    # => "hash"
p obj.then    # => "then"

これにより先程のような互換性の問題がなくなりました。
また、 OpenStruct は gem なので gem install ostruct すれば Ruby 3.0 未満でもこの機能を使うことができます。

# Ruby 2.7 でもこの機能を使える
require "ostruct"
# 各バージョン
pp RUBY_VERSION          # => "2.7.2"
pp OpenStruct::VERSION   # => "0.3.1"

# Object のメソッドと同名で定義できる
obj = OpenStruct.new(class: "class", hash: "hash", then: "then")

pp obj.class   # => "class"
pp obj.hash    # => "hash"
pp obj.then    # => "then"

これはべんり。

Object のメソッドを呼び出すには…?

Object のメソッドを明示的に呼び出す場合は呼び出したいメソッド名に ! を付けて呼び出します。

require "ostruct"
# 各バージョン
pp RUBY_VERSION          # => "3.0.0"
pp OpenStruct::VERSION   # => "0.3.1"

# Object のメソッドと同名で定義できる
obj = OpenStruct.new(class: "class", hash: "hash", then: "then")

# これは各要素を返す
pp obj.class   # => "class"
pp obj.hash    # => "hash"
pp obj.then    # => "then"

# これは Object のメソッドを呼びだす
pp obj.class!  # => OpenStruct
pp obj.hash!   # => 1305732522429585457
pp obj.then!   # => #<Enumerator: ...>

また、該当する要素がない場合も Object のメソッドを呼び出します。

require "ostruct"
# 各バージョン
pp RUBY_VERSION          # => "3.0.0"
pp OpenStruct::VERSION   # => "0.3.1"

obj = OpenStruct.new(class: "class", hash: "hash", then: "then")

# 存在しない場合は Object のメソッドを呼び出す
pp obj.to_s
# => "#<OpenStruct class=\"class\", hash=\"hash\", then=\"then\">"

# 要素を追加した場合はその要素を返す
obj.to_s = "to_s"
pp obj.to_s
# => "to_s"
pp obj.to_s!
# => "#<OpenStruct class=\"class\", hash=\"hash\", then=\"then\", to_s=\"to_s\">"

うーむ、ちょっと複雑ですね…。
ちなみに ! 付き名で定義した場合は Object のメソッドを優先します。

require "ostruct"
# 各バージョン
pp RUBY_VERSION          # => "3.0.0"
pp OpenStruct::VERSION   # => "0.3.1"

obj = OpenStruct.new(class!: "class")

# 保持してる要素ではなくて Object#class を返す
pp obj.class!
# => OpenStruct

# OpenStruct#[] でアクセスできる
pp obj[:class!]

むずかしい…。

参照