Understanding Implicit hash within a ruby method definition - ruby

I was watching the CooperPress Youtube video that discusses different ways to specify arguments with defaults within a method definition.
He discusses how in Ruby 2.0 there is some syntactic sugar which allows you to specify an implicit hash that also sets default values for each key within this hash. Located at this part of the video, but I will redisplay the code below:
def some_method(x: 10, y: 20, z: 30)
puts x
puts y
puts z
end
some_method x: 1, y: 2
=> 1
=> 2
=> 30
What I am having trouble understanding is why turning that implicit hash into an explicit hash doesn't work. Basically: all I am doing is putting curly braces around that implicit hash argument within the method definition. As I see it: putting curly braces around the key/values is just a more explicit way to say that a hash wraps those key/values and it should have the exact same behavior. However, when I do this it errors out:
def some_method({x: 10, y: 20, z: 30}) #curly braces here
puts x
puts y
puts z
end
some_method x: 1, y: 2
=> rb:1: syntax error, unexpected {, expecting ')' def some_method({x: 10, y: 20, z: 30})
Question: Why does turning that implicit hash argument within the method definition into an explicit hash argument make the method error out?

Haven't watched that video, but "implicit hash" is a poor choice of words. This is a feature called "keyword arguments" and it has a pretty specific syntax which, yes, resembles a hash, but isn't one.
For example, it allows required arguments, which is impossible in a hash.
def foo(required:, optional: 1)
# some code
end

There are special syntax rules for what you can write in a method argument definition, you cannot just write arbitrary expressions.
You could write this:
def foo(opts = arbitrary_hash)
That would mean the method has one argument and its default value is whatever arbitrary_hash is, but that would be an unusual way to do it because then if you pass any argument to the method, none of the defaults get applied.

Related

Why is Ruby's def +(other) method valid? What does "other" actually mean?

I'm practicing a Ruby program for 2D coordinate operations. Among them, the writing of def +(other) and def -(other) confuses me. I have the following three questions:
Why is the method name equal to the operator name?
Where are the parameters received? Passed from where?
Why is the parameter other called other, and what is its value and
transmission process?
This is code
class Point
attr_accessor :x, :y
def initialize(x=0, y=0)
#x, #y = x, y
end
def inspect # p印出(x, y)
"(#{x}, #{y})"
end
def +(other)
self.class.new(x + other.x, y + other.y)
end
def -(other)
self.class.new(x - other.x, y - other.y)
end
end
point0 = Point.new(3, 6)
point1 = Point.new(1, 8)
p point0
p point1
p point0 + point1
p point0 - point1
it will eventually print
(3, 6)
(1, 8)
(4, 14)
(2, -2)
Why is the method name equal to the operator name?
Why not? Why should the symbol used to define the operator not be the symbol used to call it? That's how we do it for every other method, after all: if I want to call foo, I define def foo.
The Language designers could have chosen any arbitrary name. But it just makes sense to have the name of the method be the same symbol that you use to call it. Can you imagine the confusion if the language designers had chosen, for example, the symbol - for the name of the method which corresponds to the + operator?
Other programming languages make different choices. For example, in C++, the name of the function corresponding to the + operator is operator+. In Python, the name of the method corresponding to the + operator is __add__. (More precisely, when encountering the expression a + b, Python will first try calling a.__add__(b) and if that is not implemented by a, then it will try the "reverse add" b.__radd__(a).)
In C#, there is no method name which corresponds to the operator, rather, there is an operator keyword followed by the symbol corresponding to the operator.
If you want to know all the nitty-gritty details about how operator expressions are evaluated in Ruby, I recommend checking out Section 11.4.3 Unary operator expressions and Section 11.4.4 Binary operator expressions of the ISO/IEC 30170:2012 Information technology — Programming languages — Ruby specification.
Where are the parameters received? Passed from where?
It's not quite clear what you mean by this. A parameter is kind of like a "hole" or a placeholder in the method definition. This "hole" then gets filled in with an argument when you actually call the method. For example, here:
def foo(a, b) a + b end
a and b are parameters (more precisely, mandatory positional parameters, they are mandatory because you have to pass an argument for them and they are positional because which argument gets bound to which parameter in the parameter list depends on the position of the argument in the argument list). You don't actually know what the result of this method will be because you don't know what a and b are. They are placeholders for values that will be filled in when you call the method:
foo(2, 3)
Here, 2 and 3 are arguments (more precisely, positional arguments). The argument 2 gets bound to the parameter a because 2 is the first positional argument in the argument list and a is the first positional parameter in the parameter list. The argument 3 gets bound to the parameter b because 3 is the second positional argument in the argument list and b is the second positional parameter in the parameter list.
So, the code that gets ultimately executed for this particular method call is (replacing a with 2 and b with 3):
2 + 3
Please note: this is a simplified explanation. The mental model of replacing every occurrence of the parameter in the method definition body with the argument expression is a good first approximation, but it is not actually what Ruby does. In particular, that mental model I just described corresponds to the call-by-name evaluation strategy, whereas Ruby actually uses a special case of the call-by-value evaluation strategy called call-by-object-sharing.
You can observe the difference in this code:
def bar(a) a + a end
bar((puts "Hello"; 23))
# Hello
#=> 46
In the "replace every occurrence of the parameter with the argument expression" mental model which corresponds to call-by-name, the code would look like this:
(puts "Hello"; 23) + (puts "Hello"; 23)
# Hello
# Hello
#=> 46
and Hello would be printed twice.
However, with call-by-value and call-by-object-sharing, the argument expression gets evaluated before calling the method and the result of that evaluation gets passed in, so the actual code execution looks more like this:
__fresh_variable_with_unspeakable_name__ = (puts "Hello"; 23)
# Hello
__fresh_variable_with_unspeakable_name__ + __fresh_variable_with_unspeakable_name__
#=> 46
If you want to know all the nitty-gritty details about how method arguments are evaluated in Ruby, I recommend checking out Section 11.3 Method invocation expressions, in particular Section 11.3.1 General description and Section 11.3.2 Method arguments of the ISO/IEC 30170:2012 Information technology — Programming languages — Ruby specification.
There is also some (unfortunately incomplete) information to be found in The Ruby Spec Suite aka ruby/spec, in particular in language/def_spec.rb and language/method_spec.rb.
Why is the parameter other called other, and what is its value and
transmission process?
The parameter other is called other because that is what the author of that piece of code chose to call it. They could have called it a or b or x or y or foo or bar or i_dont_have_the_slightest_idea_what_to_call_this_parameter_so_i_am_just_choosing_something_totally_random. If you want to know why the author of that piece of code chose to call it other, you would have to ask the author of that piece of code.
other is a somewhat popular name for the "other" operand of a binary operator definition, not just in Ruby, as you can see for example in the Python documentation as well. It does make sense if you read it out loud: in an Object-Oriented Programming Language like Ruby or Python, where operators are interpreted as being sent to one of the operands, one of the two operands of a binary operator will always be self or this and the "other" operand will be … well … the "other" operand. So, naming it other is just natural. In Ruby, this is codified in some Style Guides, for example the Rubocop Ruby Style Guide.

Forwarding/delegating 2 arguments in ruby

class MyClass
extend Forwardable
def_delegators :#broker, :add
def subscribe_in_broker
#subscribers.map(&method(:add))
end
end
In this example #subscribers is a Hash and #broker.add takes two arguments: def broker(k,v).
This causes ArgumentError: wrong number of arguments (given 1, expected 2)
Is there a way to use Forwardable or a similar solution without this problem? How can I easily delegate an Array with 2 elements to a method which takes two arguments? What would be the equivalent of writing:
def subscribe_in_broker
#subscribers.map{|k,v| add(k,v) }
end
but using delegation?
It might seem like the block is receiving 2 arguments, but it is actually receiving only a single argument (a 2-element array). Ruby is interpreting the |x, y| as if it was |(x, y)|. The parentheses (which can be omitted here) destructure the single argument into its two pieces.
When you use a & with a method, it also only gets only 1 argument, and if you want to destructure it, you have to be explicit by using a set of parentheses around x and y:
def add((x,y))
end
To be clear, this is a single-argument method that expects the argument to be an array. For example, you call it like this: add([1, 2])
Note: this has nothing to do with delegation, just the fact that Ruby assumes you intend to destructure in one place, while not in another.
It is possible to yield more than one argument to a block (or a method turned into a proc with '&'), but that is not what map does.
It's not obvious to me why hash.map { |x,y| add(x, y) } is not adequate for your purposes. To work around that, you'd need to define your own map function that yields 2 arguments instead of 1, and that seems like overkill for this.

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.

Trying to understand the ruby Webdriver.for method

WebDriver.for :firefox, :profile => "some-profile"
I'm new to Selenium for Ruby and i'm just trying to understand some syntax. I understand that the webdriver.for is using the first argument "firefox", but I don't understand why there is hash after the comma. Is that another argument going into the "for" method? I looked up the api docs and it shows that the "for" method only takes one argument, so I'm not exactly sure what the other argument is.
Is that another argument going into the "for" method?
Yes, in ruby if a hash argument is the last argument in the list of arguments, you don't have to write the braces. Here is an example of the different syntaxes you might see:
def do_stuff(x, y, hash)
p hash
end
do_stuff(10, 20, {:a => 30, :b => 40})
do_stuff(10, 20, :a=>30, :b=>40)
do_stuff 10, 20, :a=>30, :b=>40
do_stuff 10, 20, a:30, b:40
--output:--
{:a=>30, :b=>40}
{:a=>30, :b=>40}
{:a=>30, :b=>40}
{:a=>30, :b=>40}
Your code specifies a hash argument with only one key/value pair.

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