No Named Parameters in Ruby

No named parameters in Ruby?

What's actually happening:

# Assign a value of "meh" to scope, which is OUTSIDE meth and equivalent to
# scope = "meth"
# meth(1, scope)
meth(1, scope = "meh")

# Ruby takes the return value of assignment to scope, which is "meh"
# If you were to run `puts scope` at this point you would get "meh"
meth(1, "meh")

# id = 1, options = "meh", scope = "scope"
puts options

# => "meh"

There is no support* for named parameters (see below for 2.0 update). What you're seeing is just the result of assigning "meh" to scope being passed as the options value in meth. The value of that assignment, of course, is "meh".

There are several ways of doing it:

def meth(id, opts = {})
# Method 1
options = opts[:options] || "options"
scope = opts[:scope] || "scope"

# Method 2
opts = { :options => "options", :scope => "scope" }.merge(opts)

# Method 3, for setting instance variables
opts.each do |key, value|
instance_variable_set "@#{key}", value
# or, if you have setter methods
send "#{key}=", value
end
@options ||= "options"
@scope ||= "scope"
end

# Then you can call it with either of these:
meth 1, :scope => "meh"
meth 1, scope: "meh"

And so on. They're all workarounds, though, for the lack of named parameters.


Edit (February 15, 2013):

* Well, at least until the upcoming Ruby 2.0, which supports keyword arguments! As of this writing it's on release candidate 2, the last before the official release. Although you'll need to know the methods above to work with 1.8.7, 1.9.3, etc., those able to work with newer versions now have the following option:

def meth(id, options: "options", scope: "scope")
puts options
end

meth 1, scope: "meh"
# => "options"

Named parameters in Ruby 2

The last example you posted is misleading. I disagree that the behavior is similar to the one before. The last example passes the argument hash in as the first optional parameter, which is a different thing!

If you do not want to have a default value, you can use nil.

If you want to read a good writeup, see "Ruby 2 Keyword Arguments".

Can I have required named parameters in Ruby 2.x?

There is no specific way in Ruby 2.0.0, but you can do it Ruby 2.1.0, with syntax like def foo(a:, b:) ...

In Ruby 2.0.x, you can enforce it by placing any expression raising an exception, e.g.:

def say(greeting: raise "greeting is required")
# ...
end

If you plan on doing this a lot (and can't use Ruby 2.1+), you could use a helper method like:

def required
method = caller_locations(1,1)[0].label
raise ArgumentError,
"A required keyword argument was not specified when calling '#{method}'"
end

def say(greeting: required)
# ...
end

say # => A required keyword argument was not specified when calling 'say'

How can Ruby functions with named arguments get called with a hash instead?

Just call the method with the hash: foo(hash)

The bigger problem: You didn't use named parameters (or better keyword arguments) but parameters with default values. To use named parameters you must not not use = but :.

def foo(x: 'hi', y:'this is y')
puts x
puts y
end

hash = {x: 'first', y: 'second'}
foo(hash)

How to pass optional named arguments in Ruby 2.0

Probably the best way to go would be:

def bar(name: nil)
name ||= 'unknown'
puts name
end

Passing `nil` to method using default named parameters

If you want to follow a Object-Oriented approach, you could isolate your defaults in a separate method and then use Hash#merge:

class Taco
def initialize (args)
args = defaults.merge(args)
@meat = args[:meat]
@cheese = args[:cheese]
@salsa = args[:salsa]
end

def assemble
"taco with: #{@meat} + #{@cheese} + #{@salsa}"
end

def defaults
{meat: 'steak', cheese: true, salsa: 'spicy'}
end
end

Then following the suggestion by @sawa (thanks), use Rails' Hash#compact for your input hashes that have explicitly defined nil values and you will have the following output:

taco with: chicken + false + mild
taco with: steak + true + spicy
taco with: pork + true + spicy

EDIT:

If you do not want to use Rails' wonderful Hash#compact method, you can use Ruby's Array#compact method. Replacing the first line within the initialize method to:

args = defaults.merge(args.map{|k, v| [k,v] if v != nil }.compact.to_h)

Why does specifing a named parameter change the parameters type

Unlike in other languages (such as Python), Ruby strictly separates positional arguments and keyword arguments. You can't provide a keyword parameter as a positional argument when calling a method and vice-versa.

In your case, you have define a single positional parameter in your method (namely b). In your last two examples, you are passing a Hash to it. Note that the curly braces you generally use to define a hash are optional when calling methods and passing a Hash as its last argument. In older Ruby versions, this was used as a convention to allow/pass an optional list of arguments which works similar to keyword arguments.

As a Hash object is always truthy (remember that only false and nil are falsey in Ruby), you are always using the if branch of your conditional and outputting the information you see there.

With that being said, if you want to accept keyword arguments with your method, you have to define it accordingly, e.g.:

def test_param(b: false)
# ...
end

Note the difference in the parameter definition here. You can learn more about keyword arguments in Ruby from the Ruby language documentation.

How do I add named parameters in a subclass or change their default in Ruby 2.2?

The problem here is that since the parent doesn't know anything about the child's arguments, it has no way of knowing whether the first argument you pass to it was meant to be a positional argument, or whether it was intended to provide keyword arguments to the parent method. This is because of a historical feature where Ruby allowed hashes to be passed as keyword-argument style parameters. For example:

def some_method(options={})
puts options.inspect
end

some_method(arg1: "Some argument", arg2: "Some other argument")

Prints:

{:arg1=>"Some argument", :arg2=>"Some other argument"}

If Ruby disallowed that syntax (which would break backwards compatibility with existing programs), you could write your child method like this using the double splat operator:

class Child < Parent
def foo(*args, named1: "child", named2: "child", **keyword_args)
puts "Passing to parent: #{[*args, named1: named1, **keyword_args].inspect}"
super(*args, named1: named1, **keyword_args)
end
end

In fact, this works fine when you pass keyword arguments in addition to the positional one:

Child.new.foo({ this: 23 }, named2: "caller")

Prints:

Passing to parent: [{:this=>23}, {:named1=>"child"}]
{:this=>23}
"child"
"parent"

However, since Ruby can't tell the difference between positional arguments and keyword arguments when you only pass a single hash, Child.new.foo({ this: 23 }) results in this: 23 getting interpreted as a keyword argument by the child, and the parent method ends up interpreting both keyword arguments forwarded to it as a single positional argument (a hash) instead:

Child.new.foo({this: 23})

Prints:

Passing to parent: [{:named1=>"child", :this=>23}]
{:named1=>"child", :this=>23}
"parent"
"parent"

There are a few ways you can fix this but none of them are exactly ideal.

Solution 1

As you tried to do in your third example, you could tell the child that the first argument passed will always be a positional argument, and that the rest will be keyword args:

class Child < Parent
def foo(arg, named1: "child", named2: "child", **keyword_args)
puts "Passing to parent: #{[arg, named1: named1, **keyword_args].inspect}"
super(arg, named1: named1, **keyword_args)
end
end

Child.new.foo({this: 23})
Child.new.foo({this: 23}, named1: "custom")

Prints:

Passing to parent: [{:this=>23}, {:named1=>"child"}]
{:this=>23}
"child"
"parent"
Passing to parent: [{:this=>23}, {:named1=>"custom"}]
{:this=>23}
"custom"
"parent"

Solution 2

Switch entirely to using named arguments. This avoids the problem entirely:

class Parent
def foo(positional:, named1: "parent", named2: "parent")
puts positional.inspect
puts named1.inspect
puts named2.inspect
end
end

class Child < Parent
def foo(named1: "child", named3: "child", **args)
super(**args, named1: named1)
end
end

Child.new.foo(positional: {this: 23})
Child.new.foo(positional: {this: 23}, named2: "custom")

Prints:

{:this=>23}
"child"
"parent"
{:this=>23}
"child"
"custom"

Solution 3

Write some code to figure everything out programmatically.

This solution would likely be pretty complex and would depend a lot on exactly how you want it to work, but the idea is that you would use Module#instance_method, and UnboundMethod#parameters to read the signature of the parent's foo method and pass arguments to it accordingly. Unless you really need to do it this way, I'd recommend using one of the other solutions instead.

Named parameters in Ruby don't work?

In ruby my_method(999, var3 = 123) means, assign var3 the value 123 and pass it to my_method.

There is no concept of named parameters, however, you can use a hash as an argument to my_method such as:

def my_method(args = {})
p "#{args[:a]}, #{args[:b]}, #{args[:c]}"
end

Then you can call it with:

my_method(b: 2, c: 3, a: 1)

which will print 1, 2, 3, because b: 2, c: 3, a: 1 is inferred to be a hash by ruby. You can explicitly indicate the hash as well with:

my_method({b: 2, c: 3, a: 1})


Related Topics



Leave a reply



Submit