Today I learned that parentheses are optional in Ruby, even when defining methods, not just when you’re calling them.
Somehow that got me digging into positional arguments, named arguments and blocks so here’s a comprehensive (but pathological) example of defining a method without parentheses but with all 7 possible argument types:
def arguments required_positional,
optional_positional=2,
*other_positionals,
another_required_positional,
required_keyword:,
optional_keyword: 7,
another_required_keyword:,
**other_keywords,
&block
puts "required_positional = #{required_positional}"
puts "optional_positional = #{optional_positional}"
puts "other_positionals = #{other_positionals}"
puts "another_required_positional = #{another_required_positional}"
puts "required_keyword = #{required_keyword}"
puts "optional_keyword = #{optional_keyword}"
puts "another_required_keyword = #{another_required_keyword}"
puts "other_keywords = #{other_keywords}"
3.times(&block)
end
arguments 1, 2.0, 3, 4, 5, other_b: 10, another_required_keyword: 8,
other_a: 9, required_keyword: 6, other_c: 11 do
print 'foo '
end
# required_positional = 1
# optional_positional = 2.0
# other_positionals = [3, 4]
# another_required_positional = 5
# required_keyword = 6
# optional_keyword = 7
# another_required_keyword = 8
# other_keywords = {:other_b=>10, :other_a=>9, :other_c=>11}
# foo foo foo
You may have noticed that I passed 2.0
(as a Float) to the optional_positional
argument, which defaults to 2
(as an Integer). I did this because *other_positionals
only gets left over arguments. If I omit the 2.0
(hoping for optional_positional
to be set to it’s default value of 2
) then optional_positional
greedily grabs the first spare argument (3, in the example below), “stealing” it from the other_positionals
array:
arguments 1, 3, 4, 5, other_b: 10, another_required_keyword: 8,
other_a: 9, required_keyword: 6, other_c: 11 do
print 'bar '
end
# required_positional = 1
# optional_positional = 3
# other_positionals = [4]
# another_required_positional = 5
# required_keyword = 6
# optional_keyword = 7
# another_required_keyword = 8
# other_keywords = {:other_b=>10, :other_a=>9, :other_c=>11}
# bar bar bar
The Ruby docs state that (emphasis mine):
Prefixing an argument with * causes any remaining arguments to be converted to an Array.
The array argument must be the last positional argument, it must appear before any keyword arguments.
As you can see in the examples above, that’s not the behaviour we’re seeing. I added an additional (required) positional argument after the array argument and Ruby happily accepted it. I’m not sure if that’s a language bug or a documentation bug but given that it’s very un-idiomatic Ruby you will hopefully never see (or write!) this in the wild. Just because you can, doesn’t mean you should! :)
This is basically just a variation of the previous two observations but I’ll point it out anyway… the second required positional argument (another_required_positional
) takes precedence over optional_positional
and *other_positionals
. This makes sense (once you accept the undocumented ability to add additional required positional arguments) but I thought I’d mention it anyway. If we only provide two positional arguments they are consumed by the 1st and 4th required arguments, leaving the 2nd at its default value and the 3rd (array) empty:
arguments 1, 5, other_b: 10, another_required_keyword: 8,
other_a: 9, required_keyword: 6, other_c: 11 do
print 'baz '
end
# required_positional = 1
# optional_positional = 2
# other_positionals = []
# another_required_positional = 5
# required_keyword = 6
# optional_keyword = 7
# another_required_keyword = 8
# other_keywords = {:other_b=>10, :other_a=>9, :other_c=>11}
# baz baz baz
In case you were wondering, the undocumented behaviour for positional arguments does not hold true for keyword arguments. Adding an additional keyword argument after the keyword hash argument, **other_keywords
, causes a syntax error:
def faulty_keyword_arguments( required_keyword:, optional_keyword: 'b',
**other_keywords, another_required_keyword:)
# syntax error, unexpected tLABEL, expecting & or '&'
# ...ords, another_required_keyword:)
# ... ^
As the message suggests, the only thing allowed here is a block argument (starting with an ampersand).
That being said, other than the **other_keywords
keyword hash needing to be last in the keyword arguments, there are no restrictions on the order of optional and required keyword arguments. As you can see in the intial example, another_required_keyword
is defined after optional_keyword
. They do not need to be grouped together like positional arguments do.
You may be wondering if the old idiom of passing a hash after positional arguments works. This continues to work, but only if you don’t use any keyword arguments in the method definition:
def final_hash a, b=2, *c, &block
puts "a = #{a}"
puts "b = #{b}"
puts "c = #{c}"
end
final_hash(1, 2, 3, 4, 5, six: 6, seven: 7, eight: 8)
# a = 1
# b = 2
# c = [3, 4, 5, {:six=>6, :seven=>7, :eight=>8}]
In the example above there are only 6 arguments. The last three items are a single hash - there are implicit hash braces around them. This means all the elements of the hash must be grouped together at the end. If you try to include a positional argument amongst the hash items you’ll get an error about a missing hash rocket =>
.
If all you do with the block is call it with yield
, you don’t need to explicity name the block in the method definition:
def inline_implicit_block a, b=2, *c
puts "a = #{a}"
puts "b = #{b}"
puts "c = #{c}"
yield self
yield self
yield self
end
If you omit parentheses around the method arguments when calling the method, you can’t use the single line block syntax { print 'foo ' }
. This is why the explicit &block
examples above have the multiline do
and end
block syntax. Including the braces lets us use the single line syntax:
inline_implicit_block(1, 2, 3, 4, five: 5, six: 6) { print 'qux '}
# a = 1
# b = 2
# c = [3, 4, {:five=>5, :six=>6}]
# qux qux qux