Ruby required keyword arguments - ruby

For Ruby methods, the required keyword syntax is nice
def foo(bar:, baz:)
:
end
foo(bar: true, baz: false) # OK
foo(bar: true) # missing keyword argument error
Can one 'splat' the list of required keywords with some kind of magic? i.e.,
required_keywords = [:bar, :baz]
def foo(magic(required_keywords))
:
end
I expect not, but I'm often surprised with what Ruby can be persuaded to do.

The splat (*) and double splat (**) operators allow a method to take an arbitrary number of arguments. The former will store the arguments in an array and the latter will store them in a hash.
There is also the options hash, which is an optional hash parameter, usually included as the last parameter at the method declaration. It can also take an arbitrary number of arguments (actually, they are plain hash items).
However, in both of the above cases, the number of arguments is unknown when you declare the method. Hence, you can't make an unknown number of arguments "required".
More info on splat, double splat and required keyword parameters is available at this blog post.

Related

Why does specifing a named parameter change the parameters type

I recently ran into this and am curious if I am using named parameters incorrectly or to better understand why they behave this way.
def test_param(b=false)
if b
puts 'param is true'
puts b
puts b.class
else
puts 'param is false'
puts b
puts b.class
end
end
when I test this function in a REPL I am seeing
2.5.3 :213 > test_param(true)
param is true
true
TrueClass
=> nil
2.5.3 :214 > test_param(false)
param is false
false
FalseClass
=> nil
2.5.3 :215 > test_param(b:true)
param is true
{:b=>true}
Hash
=> nil
2.5.3 :216 > test_param(b:false)
param is true
{:b=>false}
Hash
=> nil
2.5.3 :217 >
How come when I use a named parameter the variable data type is changed to a Hash, this seems wrong.
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.

Mixing keyword argument and arguments with default values duplicates the hash?

So i discovered this ruby behaviour, which kept me going crazy for over an hour. When I pass a hash to a function which has a default value for hash AND a keyword argument, it seems like the reference doesn't get passed correctly. As soon as I take away the default value OR the keyword argument, the function behaves as expected. Am I missing some obvious ruby rule here?
def change_hash(h={}, rand: om)
h['hey'] = true
end
k = {}
change_hash(k)
k
#=> {}
It works fine as soon as I take out the default or the keyword arg.
def change_hash(h, rand: om)
h['hey'] = true
end
k = {}
change_hash(k)
k
#=> {'hey' => true}
def change_hash(h={})
h['hey'] = true
end
k = {}
change_hash(k)
k
#=> {'hey' => true}
EDIT
Thanks for your answers. Most of you pointed out that ruby parses the hash as a keyword argument in some cases. However, I am talking about the case when a hash has string keys. When I pass the hash, it seems like the value that gets passed is correct. But modifying the hash inside the function doesn't modify the original hash.
def change_hash(hash={}, another_arg: 300)
puts "another_arg: #{another_arg}"
puts "hash: #{hash}"
hash['hey'] = 3
end
my_hash = {"o" => 3}
change_hash(my_hash)
puts my_hash
Prints out
another_arg: 300
hash: {"o"=>3}
{"o"=>3}
TL;DR ruby allows passing hash as a keyword argument as well as “expanded inplace hash.” Since change_hash(rand: :om) must be routed to keyword argument, so should change_hash({rand: :om}) and, hence, change_hash({}).
Since ruby allows default arguments in any position, the parser takes care of default arguments in the first place. That means, that the default arguments are greedy and the most amount of defaults will take a place.
On the other hand, since ruby lacks pattern-matching feature for function clauses, parsing the given argument to decide whether it should be passed as double-splat or not would lead to huge performance penalties. Since the call with an explicit keyword argument (change_hash(rand: :om)) should definitely pass :om to keyword argument, and we are allowed to pass an explicit hash {rand: :om} as a keyword argument, Ruby has nothing to do but to accept any hash as a keyword argument.
Ruby will split the single hash argument between hash and rand:
k = {"a" => 42, rand: 42}
def change_hash(h={}, rand: :om)
h[:foo] = 42
puts h.inspect
end
change_hash(k);
puts k.inspect
#⇒ {"a"=>42, :foo=>42}
#⇒ {"a"=>42, :rand=>42}
That split feature requires the argument being cloned before passing. That is why the original hash is not being modified.
This is particularly tricky case in Ruby indeed.
In your example you have optional argument which is a hash and you have an optional keyword argument at the same time. In this situation if you pass only one hash, Ruby interprets it as a hash which contains keyword arguments. Here is the code to clarify:
change_hash({rand1: 'om'})
# ArgumentError: unknown keyword: rand1
To work around this you can pass two separate hashes into the method with second one (the one for keyword arguments) being empty:
def change_hash(h={}, rand: 'om')
h['hey'] = true
end
k = {}
change_hash(k, {})
k
#=> {'hey' => true}
From the practical point of view it is better to avoid metdhod signature like that in production code, because it is very easy to make an error while using the method.

Ruby method with optional options and &block parameter

Hey there
Is it possible to have optional attributes and a block as parameters
for a method call?
Example: I have to call
method(foo, foo: bar, -> { do_something }
and tried it with
def method(foo, *bar, &block)
end
As for my understanding the block always has to be at last position?
After a bit of research I found out the unary(?) * seems to be for
arrays. Since I try to pass a Hash I changed the code to
def method(foo, bar={}, &block)
end
But this doesn't do the trick either. I guess its because he cant
figure out where the bar ends and the block starts.
Any ideas or suggestions? Thank you in advance
Append: Just for the curious why I need this. We have a big json
schema running and have a small DSL that builds the json from the
model definitation. Without going to much into detail we wanted to
implement exportable_scopes.
class FooBar
exportable_scope :some_scope, title: 'Some Scope', -> { rewhere archived: true }
end
On some initializer this is supposed to happens:
def exportable_scope scope, attributes, &block
scope scope block
if attributes.any?
attributes.each do |attribute|
exportable_schema.scopes[scope] = attribute
end
else
exportable_schema.scopes[scope] = {title: scope}
end
end
So this is working fine, I just need a hint for the method
parameters.
Yes, it is possible.
When mixing different kinds of parameters, they have to be included in the method definition in a specific order:
Positional parameters (required and optional) and a single splat parameter, in any order;
Keyword parameters (required and optional), in any order;
Double splat parameter;
Block parameter (prefixed with &);
The order above is somewhat flexible. We could define a method and begin the parameter list with a single splat argument, then a couple of optional positional arguments, and so on. Even though Ruby allows that, it's usually a very bad practice as the code would be hard to read and even harder to debug. It's usually best to use the following order:
Required positional parameters;
Optional positional parameters (with default values);
Single splat parameter;
Keyword parameters (required and optional, their order is irrelevant);
Double splat parameter;
Explicit block parameter (prefixed with &).
Example:
def meditate cushion, meditation="kinhin", *room_items, time: , posture: "kekkafuza", **periods, &b
puts "We are practicing #{meditation}, for #{time} minutes, in the #{posture} posture (ouch, my knees!)."
puts "Room items: #{room_items}"
puts "Periods: #{periods}"
b.call # Run the proc received through the &b parameter
end
meditate("zafu", "zazen", "zabuton", "incense", time: 40, period1: "morning", period2: "afternoon" ) { puts "Hello from inside the block" }
# Output:
We are practicing zazen, for 40 minutes, in the kekkafuza posture (ouch, my knees!).
Room items: ["zabuton", "incense"]
Periods: {:period1=>"morning", :period2=>"afternoon"}
Hello from inside the block
Notice that when calling the method, we have:
Provided the cushion mandatory positional argument;
Overwritten the default value of the meditation optional positional argument;
Passed a couple of extra positional arguments (zabuton and incense) through the *room_items parameter;
Provided the time mandatory keyword argument;
Omitted the posture optional keyword argument;
Passed a couple of extra keyword arguments (period1: "morning", period2: "afternoon") through the **periods parameter;
Passed the block { puts "Hello from inside the block" } through the &b parameter;
Please note the example above servers only to illustrate the possibility of mixing different types of parameters. Building a method like this in real code would be a bad practice. If a method needs that many arguments, it's probably best to split it into smaller methods. If it's absolutely necessary to pass that much data to a single method, we should probably create a class to store the data in a more organized way, then pass an instance of that class to the method as a single argument.

Why are there methods that are capitalized in a list of private methods?

When I run 1.private_methods, I get the next output:
1.private_methods
=> [
:initialize_copy,
:default_src_encoding,
....
:format,
:Integer,
:Float,
:String,
:Array,
:warn,
...
:Rational,
:Complex,
:gem,
...
]
I see some methods here whose names match a class name like Integer, Float, Array, or String. How is it possible? What are they actually? Are these methods?
Yes, these are regular methods. Ruby allows capitalized method names. If it didn't, things like
Array(1..3)
# => [1, 2, 3]
wouldn't be available.
Just like a method starting with a lower case is ambiguous with a local variable, a method starting with an upper case is ambiguous with a constant (class, module being special cases). The reason you don't hit these methods when you simply write Integer, Float, etc. is because there is a rule that says to interpret them as a local variable or a constant rather than as a method when it is ambiguous. When you want to use them as a method, there is always a way to resolve the ambiguity, such as (i) attaching parentheses for arguments, (ii) writing a receiver and a period before them.

ruby - omitting default parameter before splat

I have the following method signature
def invalidate_cache(suffix = '', *args)
# blah
end
I don't know if this is possible but I want to call invalidate_cache and omit the first argument sometimes, for example:
middleware.invalidate_cache("test:1", "test")
This will of course bind the first argument to suffix and the second argument to args.
I would like both arguments to be bound to args without calling like this:
middleware.invalidate_cache("", "test:1", "test")
Is there a way round this?
Use keyword arguments (this works in Ruby 2.0+):
def invalidate_cache(suffix: '', **args) # note the double asterisk
[suffix, args]
end
> invalidate_cache(foo: "any", bar: 4242)
=> ["", {:foo=>"any", :bar=>4242}]
> invalidate_cache(foo: "any", bar: 4242, suffix: "aaaaa")
=> ["aaaaa", {:foo=>"any", :bar=>4242}]
Note that you will have the varargs in a Hash instead of an Array and keys are limited to valid Symbols.
If you need to reference the arguments by position, create an Array from the Hash with Hash#values.
How about you create a wrapper method for invalidate_cache that just calls invalidate_cache with the standard argument for suffix.
In order to do this, your code has to have some way of telling the difference between a suffix, and just another occurrence of args. E.g. In your first example, how is your program supposed to know that you didn't mean for "test:1" to actually be the suffix?
If you can answer that question, you can write some code to make the method determine at run time whether or not you provided a suffix. For example, say you specify that all suffixes have to start with a period (and no other arguments will). Then you could do something like this:
def invalidate_cache(*args)
suffix = (args.first =~ /^\./) ? args.shift : ''
[suffix, args]
end
invalidate_cache("test:1", "test") #=> ["", ["test:1", "test"]]
invalidate_cache(".jpeg", "test:1", "test") #=> [".jpeg", ["test:1", "test"]]
If, however, there actually is no way of telling the difference between an argument meant as a suffix and one meant to be lumped in with args, then you're kind of stuck. You'll either have to keep passing suffix explicitly, change the method signature to use keyword arguments (as detailed in karatedog's answer), or take an options hash.

Resources