はじめに
Railsのコードリーディングをしていたところextract_options!
というメソッドの存在を知りました。
この記事ではextract_options!
の挙動について、Railsのコードで使われている箇所とともにまとめます。
extract_options!
メソッドとは?
extract_options!
メソッドは、配列の最後の要素がハッシュであればそれを取り出し、残りの配列を返すメソッドです。
メソッド名の末尾の!
が示すように元の配列を破壊的に変更する点に注意が必要です。
サンプルコードで確認します。
sample-app> params = [:name, :age, { city: 'Tokyo', country: 'Japan' }]
=> [:name, :age, {:city=>"Tokyo", :country=>"Japan"}]
sample-app> options = params.extract_options!
=> {:city=>"Tokyo", :country=>"Japan"}
sample-app> params
=> [:name, :age]
sample-app> options
=> {:city=>"Tokyo", :country=>"Japan"}
上記の例では、extract_options!
の呼び出しによってparams
配列自体からハッシュ要素が削除され、[:name, :age]
のみが残っています。
また、最後の要素がハッシュでなければ空のハッシュを返します。
sample-app> params = [:name, :age, :city, :country]
=> [:name, :age, :city, :country]
sample-app> options = params.extract_options!
=> {}
sample-app> params
=> [:name, :age, :city, :country]
sample-app> options
=> {}
extract_options!
のコードを見てみる
extract_options!
メソッドは Rails 内で Ruby の Array クラスを拡張して実装されています。
# frozen_string_literal: true
class Hash
# By default, only instances of Hash itself are extractable.
# Subclasses of Hash may implement this method and return
# true to declare themselves as extractable. If a Hash
# is extractable, Array#extract_options! pops it from
# the Array when it is the last element of the Array.
def extractable_options?
instance_of?(Hash)
end
end
class Array
# Extracts options from a set of arguments. Removes and returns the last
# element in the array if it's a hash, otherwise returns a blank hash.
#
# def options(*args)
# args.extract_options!
# end
#
# options(1, 2) # => {}
# options(1, 2, a: :b) # => {:a=>:b}
def extract_options!
if last.is_a?(Hash) && last.extractable_options?
pop
else
{}
end
end
end
配列の末尾の要素がハッシュであるかチェックし、ハッシュの場合はpopメソッドでハッシュだけを取り除いて返しています。
extractable_options?
によって Hash のサブクラスではなく純粋な Hash インスタンスのみを対象としています。
Railsのソースコードでextract_options!
が使われているところ
Railsのソースコードでextract_options!
が使われている代表例としてvalidates
メソッドがあります。
def validates(*attributes)
defaults = attributes.extract_options!.dup # ここで使われている
validations = defaults.slice!(*_validates_default_keys)
raise ArgumentError, "You need to supply at least one attribute" if attributes.empty?
raise ArgumentError, "You need to supply at least one validation" if validations.empty?
defaults[:attributes] = attributes
validations.each do |key, options|
key = "#{key.to_s.camelize}Validator"
begin
validator = key.include?("::") ? key.constantize : const_get(key)
rescue NameError
raise ArgumentError, "Unknown validator: '#{key}'"
end
next unless options
validates_with(validator, defaults.merge(_parse_validates_options(options)))
end
end
たとえば以下のコードがあるとします。
class Book < ApplicationRecord
validates :title, presence: true
end
このとき、validates
メソッドの流れは次のようになります。
def validates(*attributes) # attributes = [:title, { presence: true }]
defaults = attributes.extract_options!.dup # ここで { presence: true } を抽出
# この時点で attributes と defaults の中身は次のようになる
# attributes = [:title]
# defaults = { presence: true }
# 後続の処理に続く...
引数attributes
からオプションの部分だけを引き抜いていますね。
このようにextract_options!
は、可変長引数(args)を取るメソッドです。
validates
メソッドのように、オプションとしてハッシュを最後に渡すパターンで頻繁に使用されます。
おわりに
extract_options!
の存在は初めて知りましたが、わりと便利そうな印象です。
オプションの指定としてハッシュを使っているメソッドは他にもあると思うので、そこでもextract_options!
が使われていないか確認してみます。
また、extract_options!
が配列を破壊的に変更する点を踏まえて使用する際には注意が必要です。