Ruby - Keyword Arguments - Can you treat all of the keyword arguments as a hash? How?

后端 未结 5 851
借酒劲吻你
借酒劲吻你 2020-12-15 16:51

I have a method that looks like this:

def method(:name => nil, :color => nil, shoe_size => nil) 
  SomeOtherObject.some_other_method(THE HASH THAT          


        
相关标签:
5条回答
  • 2020-12-15 17:14

    How about the syntax below?

    For it to work, treat params as a reserved keyword in your method and place this line at the top of the method.

    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 讨论(0)
  • 2020-12-15 17:19

    @Dennis 's answer is useful and educational. However, I noticed that Binding#local_variables will return all the local variables, regardless of when local_variables is executed:

    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
    

    I propose a hybrid solution:

    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 讨论(0)
  • 2020-12-15 17:26

    Of course! Just use the double splat (**) operator.

    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"}
    

    It works just like the regular splat (*), used for collecting parameters. You can even forward the keyword arguments to another method.

    def forward_all(*arguments, **keyword_arguments, &block)
      SomeOtherObject.some_other_method *arguments,
                                        **keyword_arguments,
                                        &block
    end
    
    0 讨论(0)
  • 2020-12-15 17:26

    I had some fun with this, so thanks for that. Here's what I came up with:

    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
    

    I chose to return an array, but you could easily modify this to return a hash instead (for instance, by not caring about the argument type after the initial detection).

    0 讨论(0)
  • 2020-12-15 17:30

    Yes, this is possible, but it's not very elegant.

    You'll have to use the parameters method, which returns an array of the method's parameters and their types (in this case we only have keyword arguments).

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

    Knowing that, there's various ways how to use that array to get a hash of all the parameters and their provided values.

    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}
    

    So your example would look like

    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
    

    Think carefully about using this. It's clever but at the cost of readability, others reading your code won't like it.

    You can make it slightly more readable with a helper method.

    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
    

    Update: Ruby 2.2 introduced Binding#local_variables which can be used instead of Method#parameters. Be careful because you have to call local_variables before defining any additional local variables inside the method.

    # 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}
    
    0 讨论(0)
提交回复
热议问题