Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

[Ruby] Hashie::Mashのすすめ

こんにちは。お久しぶりの kazz です。
本日はHashにひと味ついた Hashie のご紹介です

hashie/hashie - GitHub

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使います


関連記事

Ruby: アンパサンドとコロン`&:`記法について調べてみた


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。