ruby - omitting default parameter before splat - ruby

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.

Related

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 required keyword arguments

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.

How does [ ] work on a class in Ruby

I see that I can get a list of files in a directory using
Dir["*"]
How am I supposed to read that syntax exactly ? As I know that you can use [ ] to fetch a value from a array or a hash.
How does [ ] work on a call ?
[] is simply a method, like #to_s, #object_id. etc.
You can define it on any object:
class CoolClass
def [](v)
puts "hello #{v}"
end
end
CoolClass.new["John"] # => "hello John"
In your case it's defined as singleton method, in this way:
class Dir
def self.[](v)
...
end
end
From the Ruby Docs, Dir["*"] is equivalent to Dir.glob(["*"]). (As pointed out, it's syntactic sugar)
Dir isn't a call, it's a class, and objects of class Dir are directory streams, which you access like an array.
In your specific case, Dir["*"] will return an array of filenames that are found from the pattern passed as Dir[patternString]. "*" as a pattern will match zero or more characters, in other words, it will match everything, and thus will return an array of all of the filenames in that directory.
For your second question, you can just define it as any other method like so:
class YourClass
def self.[](v)
#your code here
end
end
The method Dir::glob takes an argument, and provides an array of all directories and files nested under the argument. (From there, you can grab the index of the array with [0].) The argument may include a pattern to match, along with flags. The argument (pattern, flags) may be options similar (but not exactly) regular expressions.
From the docs, including a couple of patterns/flags that may be of interest to you:
Note that this pattern is not a regexp, it's closer to a shell glob. See File.fnmatch for the meaning of the flags parameter. Note that case sensitivity depends on your system (so File::FNM_CASEFOLD is ignored), as does the order in which the results are returned.
* - Matches any file. Can be restricted by other values in the glob. Equivalent to / .* /x in regexp.
[set] - Matches any one character in set. Behaves exactly like character sets in Regexp, including set negation ([^a-z]).
The shorthand of Dir::glob() is Dir[], although I prefer the long form. As you saw above, using brackets denotes a special pattern/flag for the argument. Here are some examples (from the docs) that may better explain this:
Dir["config.?"] #=> ["config.h"]
Dir.glob("config.?") #=> ["config.h"]
Dir.glob("*.[a-z][a-z]") #=> ["main.rb"]
Dir.glob("*") #=> ["config.h", "main.rb"]
It is possible for you to redefine the [] method for Dir, but I will not show how -- many (and myself) do not recommend monkey-patching core Ruby classes and modules. However, you can create the method in a class of your own. See the following:
class User
# Class method => User.new[arg]
def self.[](arg)
end
# Instance method => #user[arg]
def [](arg)
end
end
Dir is an object just like any other object (it just happens to be an instance of class Class), and [] is a method just like any other method (it just happens to have a funny name, and special syntactic conveniences that allow it to called using a different syntax in addition to the normal one).
So, you define it just like any other method:
class MyClass
def self.[](*) end
end

How to pass an optional argument into a Ruby method?

I'm working on some Watir-webdriver tests in Ruby and can't seem to get the following code to work. I want to pass an optional validation argument into the log_activity method.
def log_activity (type, *validation)
#do something
end
I pass the following arguments into the method:
log_activity("license", 1)
I expect validation == 1 to be true, but it is false:
puts validation.empty?
-> false
puts validation
-> 1
if validation == 1
puts "validation!!!!"
else
puts "WTF"
end
-> WTF
What am I doing wrong?
Forgot to mention, I'm using ruby 1.9.3
*validation is an array that includes the second and all arguments afterwards. Given that it is an array, the results you see make sense. You want to check the first element in the *validation array.
Alternatively, if you will only get one optional argument, you can do:
def log_activity (type, validation=nil)
#do something
end
Then validation will be whatever you passed in.
Read "Method Arguments In Ruby" and look at "Optional Arguments". I found it pretty handy.
I am pasting the useful content:
Optional Arguments
If you want to decide at runtime how many – if any – arguments you will supply to a method, Ruby allows you to do so. You need to use a special notation when you define the method, e.g.:
def some_method(*p)
end
You can call the above method with any number of arguments (including none), e.g.:
some_method
or
some_method(25)
or
some_method(25,"hello", 45, 67)
All of those will work. If no arguments are supplied, then p will be an empty array, otherwise, it will be an array that contains the values of all the arguments that were passed in.
when you use *args as the last argument in Ruby, args is an array.
Unfortunately for you, on Ruby 1.8, array.to_s == array.join("")
Try either
if validation == [1]
or
if validation.first == 1

Passing a hash to a function ( *args ) and its meaning

When using an idiom such as:
def func(*args)
# some code
end
What is the meaning of *args? Googling this specific question was pretty hard, and I couldn't find anything.
It seems all the arguments actually appear in args[0] so I find myself writing defensive code such as:
my_var = args[0].delete(:var_name) if args[0]
But I'm sure there's a better way I'm missing out on.
The * is the splat (or asterisk) operator. In the context of a method, it specifies a variable length argument list. In your case, all arguments passed to func will be putting into an array called args. You could also specify specific arguments before a variable-length argument like so:
def func2(arg1, arg2, *other_args)
# ...
end
Let's say we call this method:
func2(1, 2, 3, 4, 5)
If you inspect arg1, arg2 and other_args within func2 now, you will get the following results:
def func2(arg1, arg2, *other_args)
p arg1.inspect # => 1
p arg2.inspect # => 2
p other_args.inspect # => [3, 4, 5]
end
In your case, you seem to be passing a hash as an argument to your func, in which case, args[0] will contain the hash, as you are observing.
Resources:
Variable Length Argument List, Asterisk Operator
What is the * operator doing
Update based on OP's comments
If you want to pass a Hash as an argument, you should not use the splat operator. Ruby lets you omit brackets, including those that specify a Hash (with a caveat, keep reading), in your method calls. Therefore:
my_func arg1, arg2, :html_arg => value, :html_arg2 => value2
is equivalent to
my_func(arg1, arg2, {:html_arg => value, :html_arg2 => value2})
When Ruby sees the => operator in your argument list, it knows to take the argument as a Hash, even without the explicit {...} notation (note that this only applies if the hash argument is the last one!).
If you want to collect this hash, you don't have to do anything special (though you probably will want to specify an empty hash as the default value in your method definition):
def my_func(arg1, arg2, html_args = {})
# ...
end

Resources