web-dev-qa-db-ja.com

Ruby-キーワード引数-すべてのキーワード引数をハッシュとして扱うことができますか?どのように?

私は次のようなメソッドを持っています:

def method(:name => nil, :color => nil, shoe_size => nil) 
  SomeOtherObject.some_other_method(THE HASH THAT THOSE KEYWORD ARGUMENTS WOULD MAKE)
end

任意の呼び出しに対して、オプション値の任意の組み合わせを受け入れることができます。名前付き引数が好きです。メソッドのシグネチャを見て、使用可能なオプションを確認できるからです。

私が知らないのは、上記のコードサンプルで大文字で説明したものへのショートカットがあるかどうかです。

昔は、次のようになりました。

def method(opts)
  SomeOtherObject.some_other_method(opts)
end

エレガントでシンプル、ほとんど浮気。

それらのキーワード引数のショートカットはありますか、それともメソッド呼び出しでオプションハッシュを再構成する必要がありますか?

31
Jesse Farmer

はい、これは可能ですが、あまりエレガントではありません。

parametersメソッド を使用する必要があります。これは、メソッドのパラメーターとそのタイプの配列を返します(この場合、キーワード引数のみがあります)。

def foo(one: 1, two: 2, three: 3)
  method(__method__).parameters
end  
#=> [[:key, :one], [:key, :two], [:key, :three]]

それを知っていると、その配列を使用してすべてのパラメーターとそれらに提供された値のハッシュを取得する方法はさまざまです。

def foo(one: 1, two: 2, three: 3)
  params = method(__method__).parameters.map(&:last)
  opts = params.map { |p| [p, eval(p.to_s)] }.to_h
end
#=> {:one=>1, :two=>2, :three=>3}

だからあなたの例は次のようになります

def method(name: nil, color: nil, shoe_size: nil)
  opts = method(__method__).parameters.map(&:last).map { |p| [p, eval(p.to_s)] }.to_h
  SomeOtherObject.some_other_method(opts)
end

これの使用について慎重に検討してください。それは賢いですが、読みやすさを犠牲にして、あなたのコードを読んでいる他の人はそれを気に入らないでしょう。

ヘルパーメソッドを使用すると、少し読みやすくすることができます。

def params # Returns the parameters of the caller method.
  caller_method = caller_locations(length=1).first.label  
  method(caller_method).parameters 
end

def method(name: nil, color: nil, shoe_size: nil)
  opts = params.map { |p| [p, eval(p.to_s)] }.to_h
  SomeOtherObject.some_other_method(opts)
end

更新:Ruby 2.2はBinding#local_variablesの代わりに使用できるMethod#parametersを導入しました。メソッド内で追加のローカル変数を定義する前にlocal_variablesを呼び出す必要があるため、注意が必要です。

# Using Method#parameters
def foo(one: 1, two: 2, three: 3)
  params = method(__method__).parameters.map(&:last)
  opts = params.map { |p| [p, eval(p.to_s)] }.to_h
end
#=> {:one=>1, :two=>2, :three=>3}

# Using Binding#local_variables (Ruby 2.2+)
def bar(one: 1, two: 2, three: 3)
  binding.local_variables.params.map { |p|
    [p, binding.local_variable_get(p)]
  }.to_h
end
#=> {:one=>1, :two=>2, :three=>3}
17
Dennis

もちろん!ダブルスプラットを使用するだけです(**)演算子。

def print_all(**keyword_arguments)
  puts keyword_arguments
end

def mixed_signature(some: 'option', **rest)
  puts some
  puts rest
end

print_all example: 'double splat (**)', arbitrary: 'keyword arguments'
# {:example=>"double splat (**)", :arbitrary=>"keyword arguments"}

mixed_signature another: 'option'
# option
# {:another=>"option"}

通常のスプラットと同じように機能します(*)、パラメータの収集に使用されます。キーワード引数を別のメソッドに転送することもできます。

def forward_all(*arguments, **keyword_arguments, &block)
  SomeOtherObject.some_other_method *arguments,
                                    **keyword_arguments,
                                    &block
end
8
Matheus Moreira

楽しかったので、ありがとうございました。これが私が思いついたものです:

describe "Argument Extraction Experiment" do
  let(:experiment_class) do
    Class.new do
      def method_with_mixed_args(one, two = 2, three:, four: 4)
        extract_args(binding)
      end

      def method_with_named_args(one:, two: 2, three: 3)
        extract_named_args(binding)
      end

      def method_with_unnamed_args(one, two = 2, three = 3)
        extract_unnamed_args(binding)
      end

      private

      def extract_args(env, depth = 1)
        caller_param_names = method(caller_locations(depth).first.label).parameters
        caller_param_names.map do |(arg_type,arg_name)|
          { name: arg_name, value: eval(arg_name.to_s, env), type: arg_type }
        end
      end

      def extract_named_args(env)
        extract_args(env, 2).select {|arg| [:key, :keyreq].include?(arg[:type]) }
      end

      def extract_unnamed_args(env)
        extract_args(env, 2).select {|arg| [:opt, :req].include?(arg[:type]) }
      end
    end
  end

  describe "#method_with_mixed_args" do
    subject { experiment_class.new.method_with_mixed_args("uno", three: 3) }
    it "should return a list of the args with values and types" do
      expect(subject).to eq([
        { name: :one,    value: "uno", type: :req },
        { name: :two,    value: 2,     type: :opt },
        { name: :three,  value: 3,     type: :keyreq },
        { name: :four,   value: 4,     type: :key }
      ])
    end
  end

  describe "#method_with_named_args" do
    subject { experiment_class.new.method_with_named_args(one: "one", two: 4) }
    it "should return a list of the args with values and types" do
      expect(subject).to eq([
        { name: :one,    value: "one", type: :keyreq },
        { name: :two,    value: 4,     type: :key },
        { name: :three,  value: 3,     type: :key }
      ])
    end
  end

  describe "#method_with_unnamed_args" do
    subject { experiment_class.new.method_with_unnamed_args(2, 4, 6) }
    it "should return a list of the args with values and types" do
      expect(subject).to eq([
        { name: :one,    value: 2,  type: :req },
        { name: :two,    value: 4,  type: :opt },
        { name: :three,  value: 6,  type: :opt }
      ])
    end
  end
end

配列を返すことを選択しましたが、代わりにハッシュを返すようにこれを簡単に変更できます(たとえば、最初の検出後に引数の型を気にしないことで)。

1
Chris Cashwell

@Dennisの答えは有用で教育的です。ただし、Binding#local_variablesがいつ実行されるかに関係なく、local_variablesがローカル変数を返すことに気付きました:all

def locals_from_binding(binding_:)
  binding_.local_variables.map { |var|
    [var, binding_.local_variable_get(var)]
  }.to_h
end

def m(a:, b:, c:)
  args = locals_from_binding(binding_: binding)
  pp args

  d = 4
end

m(a: 1, b: 3, c: 5)
# Prints:
#   {:a=>1, :b=>3, :c=>5, :args=>nil, :d=>nil}
# Note the presence of :d

ハイブリッドソリューションを提案します。

def method_args_from_parameters(binding_:)
  method(caller_locations[0].label)
  .parameters.map(&:last)
  .map { |var|
    [var, binding_.local_variable_get(var)]
  }.to_h
end

def m(a:, b:, c:)
  args = method_args_from_parameters(binding_: binding)
  pp args

  d = 4
end

m(a: 1, b: 3, c: 5)
# Prints:
#   {:a=>1, :b=>3, :c=>5}
# Note the absence of :d
0
Pistos

以下の構文はどうですか?

これを機能させるには、paramsをメソッドの予約キーワードとして扱い、この行をメソッドの先頭に配置します。

def method(:name => nil, :color => nil, shoe_size => nil) 
  params = params(binding)

  # params now contains the hash you're looking for
end

class Object
  def params(parent_binding)
    params = parent_binding.local_variables.reject { |s| s.to_s.start_with?('_') || s == :params }.map(&:to_sym)

    return params.map { |p| [ p, parent_binding.local_variable_get(p) ] }.to_h
  end
end
0
Abdo