2020年10月12日月曜日

Ruby の hashie を使ってハッシュを使いやすくする

概要

hashie/hashie は Ruby のハッシュを拡張するためのライブラリです
ハッシュのフィールドにアクセスするときにスクエアブラケットではなくドットにしたりハッシュに対して型成約を付けたりすることができます
今回はいろいろな使い方のサンプルを紹介します

環境

  • macOS 10.15.7
  • Ruby 2.7.1p83

Getting Started

とりあえず使ってみましょう
既存のハッシュから Hashie::Mash オブジェクトを作成すればキーにドットでアクセスできるようになります
ネストしている Array は Hashie::Array、Hash は Hashie::Mash として変換されています

require 'hashie'

profile = Hashie::Mash.new({
  name: 'hawk',
  age: 10,
  langs: [
    'ruby',
    'swift',
    'python'
  ],
  score: {
    'japanese': 10,
    'arithmetic': 20,
    'science': 30,
  }
})
puts profile.name
puts profile.age
puts profile.langs.first
puts profile.score.science

型を強制する

ハッシュには型がありませんが Coercion という機能を使うと入力するハッシュの型成約ができます
Hashie::Extensions::CoercionHashie::Extensions::MergeInitializer を include しましょう
そして coerce_key を使って型成約するフィールドを指定します

class Profile < Hash
  include Hashie::Extensions::Coercion
  include Hashie::Extensions::MergeInitializer

  coerce_key :name, String
  coerce_key :age, Integer
end

profile = Profile.new({
  name: 'hawk',
  age: 10
})
puts profile[:name]
puts profile[:age]

強制した上でドットでフィールドにアクセスする

先程は普通にハッシュに対して型成約しただけなのでフィールドにアクセスする場合はスクウェアブラケットを使います
それだと Hashie っぽくないのでドットでアクセスできるようにする場合は Hashie::Mash を継承しましょう

class Profile < Hashie::Mash
  include Hashie::Extensions::Coercion
  include Hashie::Extensions::MergeInitializer

  coerce_key :name, String
  coerce_key :age, Integer
end

profile = Profile.new({
  name: 'hawk',
  age: 10
})
puts profile.name
puts profile.age

強制なので強制できない場合は変換メソッドのデフォルトの値になる

例えば coerce_key :age, Integer した場合は age フィールドには数値が入ることが想定されます
しかし以下のように数値以外が来た場合には to_i メソッドの返り値がそのまま入ります

class Profile < Hashie::Mash
  include Hashie::Extensions::Coercion
  include Hashie::Extensions::MergeInitializer

  coerce_key :name, String
  coerce_key :age, Integer
end

profile = Profile.new({
  name: 'hawk',
  age: 'snowlog'
})
puts profile.name
puts profile.age # -> 0

この場合 age は 0 で初期化されます
なぜなから 'snowlog'.to_i の結果が 0 になるからです

クラスの入れ子にすると自動で初期化してくれる

例えば強制するハッシュクラス内に別のクラスがある場合にはそのクラスの initialize を自動で読んでセットしてくれます

class Score < Hashie::Mash
  include Hashie::Extensions::Coercion
  include Hashie::Extensions::MergeInitializer

  coerce_key :japanese, Integer
  coerce_key :arithmetic, Integer
  coerce_key :science, Integer
end

class Profile < Hashie::Mash
  include Hashie::Extensions::Coercion
  include Hashie::Extensions::MergeInitializer

  coerce_key :name, String
  coerce_key :age, Integer
  coerce_key :score, Score
end

profile = Profile.new({
  name: 'hawk',
  age: 10,
  score: {
    japanese: 20,
    arithmetic: 30,
    science: 40
  }
})
puts profile.name
puts profile.age
puts profile.score.japanese

入れ子になるクラスは Hashie::Mash を継承していなくてもいい

入れ子にするクラスは既存のクラスを使いたい場合もあると思います
そんな場合はハッシュを受取る initialize を定義すればそちらを自動で読んでくれます

class Score
  attr_accessor :japanese, :arithmetic, :science

  def initialize(score)
    @japanese = score[:japanese]
    @arithmetic = score[:arithmetic]
    @science = score[:science]
  end
end

class Profile < Hashie::Mash
  include Hashie::Extensions::Coercion
  include Hashie::Extensions::MergeInitializer

  coerce_key :name, String
  coerce_key :age, Integer
  coerce_key :score, Score
end

profile = Profile.new({
  name: 'hawk',
  age: 10,
  score: {
    japanese: 20,
    arithmetic: 30,
    science: 40
  }
})
puts profile.name
puts profile.age
puts profile.score.japanese

coerce_key は lambda を受け取ることもできる

型が 1 つじゃない可能性がある場合は coerce_key を lambda で定義することもできます

class Profile < Hashie::Mash
  include Hashie::Extensions::Coercion
  include Hashie::Extensions::MergeInitializer

  coerce_key :name, String
  coerce_key :age, Integer
  coerce_key :point, lambda { |v|
    if v > 0
      v
    else
      'error'
    end
  }
end

profile = Profile.new({
  name: 'hawk',
  age: 'snowlog',
  point: -1
})
puts profile.name
puts profile.age
puts profile.point

アロー演算子を使った場合は以下のように定義できます

class Profile < Hashie::Mash
  include Hashie::Extensions::Coercion
  include Hashie::Extensions::MergeInitializer

  coerce_key :name, String
  coerce_key :age, Integer
  coerce_key :point, ->(v) do
    if v > 0
      v
    else
      'error'
    end
  end
end

最後に

Ruby の Hashie を使ってハッシュの拡張をしてみました
とりあえずハッシュのフィールドにドットでアクセスするだけでも便利かなと思います
他にもキーが存在しない場合にはエラーにしたり定義していないキーを無視したりといろいろな機能があります

ハッシュの自由度を少し制限してしまう感じもあるので型強制については使いすぎると Ruby の良さも失ってしまうかもしれません
また型強制することで本来入らないであろうデータが入ってきてエラーを握りつぶす可能性もあるのでその辺りの考慮も必要になるかなと思います
リレーショナルなデータベースなどで型が厳密に決まっている場合には使えそうな気がします

参考サイト

0 件のコメント:

コメントを投稿