pkilpo
pkilpo

Reputation: 3

Rails method parameter call convention problems

I have following method with lots of parameters. I have converted this method from parameter list to Hash to avoid mess with default parameters.

 def foo_bar(options = {})
    Rails.logger.info("Got params #{options.inspect}")

    good_name = options[:foo1]
    another_param = options[:foo2]
    yet_another = options[:foo3] || "yes"
    still_one = options[:foo3]
    valid_name = options[:foo4]
    easy_name = options[:foo5]
    mode = options[:foo6]
..

Calling this method is now easy, using just params that are necessary in context, that is good

    foo_bar(foo1: my_foo1, foo3: my_foo3, foo6: my_mode)

But my question is, since Rails is often very intuitive language where you have many shortcuts and ways to do things, how to make this method param signature more clear so

want to avoid this since cannot freely choose which params I want assign value to when calling this method. Also call would not be clear which params is which since they are not referenced by name, only by value and order.

def foo_bar(good_name, another_param = nil, yet_another = "yes", still_one = "xx", valid_name = nil, easy_name = nil, foo6 = "std")

Method overloading is not working here either tried that too. That is not implemented like in C# or Java.

I have tried: Method overloading Parameters as hash Parameters "normal way"

Upvotes: 0

Views: 72

Answers (1)

max
max

Reputation: 102001

The modern way of declaring the method would be to use keyword arguments instead of a hash as a positional argument.

def foo_bar(**options)
  # ...
end

One of the issues with the positional argument approach is that you can actually pass input that's not a hash and it will blow up inside the method instead of getting an argument error which will immediately tell you what's wrong.

** is a double splat and means that we are slurping up any keyword arguments passed to the method.

There are many ways of handling default values. If you don't want to list them explicitly in the method definition (def foo(bar: :Baz, ...)) you can have a private method which produces the default options:

class Foo
  def bar(**kwargs)
    options = kwargs.reverse_merge(default_options)
    # ...
  end

  private

  def default_options
    { 
      a: 1,
      b: 2
    }
  end
end

This lets subclasses override the method and is found all over the Rails codebase.

reverse_merge is from ActiveSupport, you can do the same thing with merge in plain Ruby but in the reverse order.

I don't have to extract params from hash, they would automatically be assigned to correcponding local var

This pattern is commonly referred to as mass assignment:

class Thing
  attr_accessor :foo
  attr_accessor :bar

  def initialize(**attributes)
    attributes.each do |key, value|
      send("#{key}=", value)
    end 
  end 
end 

Thing.new(foo: "a", bar: "b")

For every keyword argument passed to the method we attempt to call a setter metod with the same name as the key. While you can call instance_variable_set and set Ivars directly that doesn't allow you to control what can be assigned and how.

In Rails you get this for free in your models from ActiveModel::AttributeAssignment and it can be mixed into any class if desired.

Upvotes: 1

Related Questions