こんにちは。お久しぶりの kazz です。
本日はHashにひと味ついた Hashie のご紹介です
Hashの基本
hash = { a: 1, b: 2, c: 3 }
p hash[:a] # => 1
hash[:d] = 4
p hash # {:a=>1, :b=>2, :c=>3, :d=>4}
p hash[:d] #=> 4
p hash[:x] # => nil
p hash.a #=> NoMethodError
Hashはkey-value形式でデータを保持できる便利な入れ物ですが、ご承知の通り、キーをメソッドとして実行することはできません
Hashie::Mashでキーをメソッドとして実行
require 'hashie'
mash = Hashie::Mash.new(a: 1, b: 2, c: 3)
p mash[:a] # => 1
mash[:d] = 4
p mash # #<Hashie::Mash a=1 b=2 c=3 d=4>
p mash[:d] #=> 4
p mash[:x] # => nil
p mash.a #=> 1
mash.e = 5
p mash # #<Hashie::Mash a=1 b=2 c=3 d=4 e=5>
p mash.e #=> 5
p mash.x #=> nil
本日の話題 Hashie::Mash は、Hashの機能を損なわずにキーをメソッドとしてアクセスできるようになります
Hashキーをメソッドとしてコールしたいモチベーション
ネストの途中でぼっち演算子を使いたい
Hash
alice = {
name: 'alice',
profile: { birth_on: '1979-01-31' }
}
bob = {
name: 'bob'
# no profile
}
p alice[:profile][:birth_on] #=> "1979-01-31"
p bob[:profile][:birth_on] #=> undefined method `[]' for nil:NilClass (NoMethodError)
# bob[:profile]&.[:birth_on] # は構文エラー
このように、ネストしたHashの途中でnilが返ると途端に面倒になります。
この場合は Hash#dig
に置き換える必要に迫られます
p alice.dig(:profile, :birth_on) #=> "1979-01-31"
p bob.dig(:profile, :birth_on) #=> nil
Hashie::Mash
require 'hashie'
alice = Hashie::Mash.new(
name: 'alice',
profile: { birth_on: '1979-01-31' }
)
bob = Hashie::Mash.new(
name: 'bob'
# no profile
)
p alice.profile&.birth_on #=> "1979-01-31"
p bob.profile&.birth_on #=> nil
一方Hashie::Mashの場合、キーをメソッドとして取り扱えるのでぼっち演算子が使えます
&:symbol
構文を使いたい
Hashキーをメソッドとしてアクセスできるようになるとblockをとる集約関数に威力を発揮します。
Hash
list = [
{ name: 'apple', type: :fruit, color: :red },
{ name: 'cherry', type: :flower, color: :red },
{ name: 'banana', type: :fruit, color: :yellow },
{ name: 'dandelion', type: :flower, color: :yellow },
]
p list.sort_by{ |h| h[:name] }
.index_by{ |h| h[:name] }
.transform_values{ |v| v[:color] }
# => {
# "apple"=>:red,
# "banana"=>:yellow,
# "cherry"=>:red,
# "dandelion"=>:yellow
# }
上記はHashリストを、キーを名前、値と色した、辞書順のHashに変換するロジックになります。ブロック内のハッシュアクセスの記述が煩わしく感じます
Hashie::Mash
Hashie::Mashを用いてキーをメソッドとして呼び出せるようになると、
同じロジックがスッキリ記述できるようになります
require 'hashie'
mash_list = [
Hashie::Mash.new(name: 'apple', type: :fruit, color: :red),
Hashie::Mash.new(name: 'cherry', type: :flower, color: :red),
Hashie::Mash.new(name: 'banana', type: :fruit, color: :yellow),
Hashie::Mash.new(name: 'dandelion', type: :flower, color: :yellow),
]
p list.sort_by(&:name).index_by(&:name).transform_values(&:color)
# =>{
# "apple"=>:red,
# "banana"=>:yellow,
# "cherry"=>:red,
# "dandelion"=>:yellow
# }
OpenStructとの違い
Hashを継承しているのでHashのメソッドをそのまま使える
Hashには、 #merge
や #transform_values
など強力な変換メソッドが実装されていますが、Hashを継承しないOpenStructを使っている場合は一度Hashに戻してやらないといけません
OpenStruct
require 'ostruct'
os = OpenStruct.new(a: 1, b: 2, c: 3)
p os.transform_values(&:succ) #=> nil
# os[:transform_values] の値(=nil) が返る。
# readerに渡したブロックは評価されない
p OpenStruct.new(
os.to_h.transform_values(&:succ)
) #=> #<OpenStruct a=2, b=3, c=4>
p os.merge(d: 4, e: 5) #=> NoMethodError
p OpenStruct.new(
os.to_h.merge(d: 4, e: 5)
) #<OpenStruct a=1, b=2, c=3, d=4, e=5>
Hashie::Mash
require 'hashie'
mash = Hashie::Mash.new(a: 1, b: 2, c: 3)
p mash.transform_values(&:succ) #=> <Hashie::Mash a=2 b=3 c=4>
p mash.merge(d: 4, e: 5) #=> #<Hashie::Mash a=1 b=2 c=3 d=4 e=5>
子要素にHashをセットすると勝手にMash化してくれる
OpenStructの場合は、値にHashをセットするとそのままHashがセットされますが、Hashie::Mashの場合は、セットしたHashは自動的にHashie::Mashに変換されます
OpenStruct
require 'ostruct'
os = OpenStruct.new(a: 1, b: 2, c: 3)
os.sub = { pi: 3.14 }
p os #=> #<OpenStruct a=1, b=2, c=3, sub={:pi=>3.14}>
p os.sub[:pi] #=> 3.14
p os.sub.pi #=> undefined method `pi' for {:pi=>3.14}:Hash (NoMethodError)
Hashie::Mash
require 'hashie'
mash = Hashie::Mash.new(a: 1, b: 2, c: 3)
mash.sub = { pi: 3.14 }
p mash #=> #<Hashie::Mash a=1 b=2 c=3 sub=#<Hashie::Mash pi=3.14>>
p mash.sub[:pi] #=> 3.14
p mash.sub.pi #=> 3.14
p mash.dig(:sub, :pi) #=> 3.14
Hashie::Mashの仲間たち
HashieはMash以外にも以下のクラスが用意されています。簡単に用例をご紹介します
Hashie::Mash
Hashie::Dash
Hashie::Trash
Hashie::Clash
Hashie::Rash
Hashie::Mash
メソッドアクセスできるHash
Hashie::Mash.new(a: 1).a #=> 1
Hashie::Dash
プロパティ定義できるHash
class Product < Hashie::Dash
include Hashie::Extensions::Dash::Coercion
property :name, required: true
property :price, coerce: Integer
property :tax, default: 1.1
def price_with_tax
(price * tax).ceil if price
end
end
p Product.new(name: '商品A', price: '1000').price_with_tax #=> 1100
Hashie::Trash
Hashie::Dashのプロパティ名を変換できる
class Product < Hashie::Trash
include Hashie::Extensions::IndifferentAccess
include Hashie::Extensions::Dash::Coercion
property :name, required: true, from: :productName
property :price, coerce: Integer
property :tax, default: 1.1
def price_with_tax
(price * tax).ceil if price
end
end
require 'json'
json = %({"productName":"商品A","price":"1000"})
p Product.new(JSON.parse(json)).name #=> "商品A"
Hashie::Clash
複雑なHashをワンライナーで生成できる
p Hashie::Clash
.new
.joins(:seller, :provider)
.where(type: :flower)
.where(color: :red)
.order(:name, type: :desc)
#=>
{
:joins=>[:seller, :provider],
:where=>{:type=>:flower, :color=>:red},
:order=>[:name, {:type=>:desc}]
}
Hashie::Rash
正規表現をキーにできる
integer_type = Hashie::Rash.new(
/\A0\z/ => 'zero',
/\A-\d+\z/ => 'negative',
/\A\d+\z/ => 'positive',
/.*/ => proc { |m| "#{m} is not a integer" }
)
p integer_type['0'] #=> "zero"
p integer_type['387'] #=> "positive"
p integer_type['-700'] #=> "negative"
p integer_type['8.2'] #=> "8.2 is not a integer"
最後に
これからはOpenStructはやめてHashie使います