Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ruby - Mixing named and positional parameters, why does order matter?

Tags:

ruby

Creating a function with both named and positional parameters

class Foo
  def initialize(bar:, bang:, bamph, &block)
    # ...
  end
end

Produces a syntax error:

$ ruby -c scratch.rb
scratch.rb:2: syntax error, unexpected tIDENTIFIER
...f initialize(bar:, bang:, bamph, &block)
...                          ^~~~~

Wheras this

 def initialize(bamph, bar:, bang:,  &block)
    # ...
 end

does not.

As far as I can see, this answer explains that the ordering of the parameter type must follow a specific pattern. But what benefit is gained by enforcing this hierarchy?

like image 258
0112 Avatar asked Feb 26 '26 01:02

0112


1 Answers

Keyword parameters and arguments are relatively new in Ruby. They were only introduced in Ruby 2.0.

Before Ruby had keyword parameters and arguments, there was a widely-used idiom of passing a Hash literal as the last argument of the method. This idiom looked something like this:

DEFAULTS = {
  :mode => 'w',
  :eol => :crlf,
}

def open_file(name, options = {})
  raise ArgumentError unless options[:encoding]
  options = DEFAULTS.merge(option)
  mode, eol, encoding = options[:mode], options[:eol], options[:encoding]
  # do your thing
end

open_file('test.txt', { :mode => 'r', :encoding => 'UTF-8' })

In order to make it look a bit more "keyword-like", you are allowed to leave out the parentheses if you pass a Hash literal as the very last argument of a message send:

open_file('test.txt', :mode => 'r', :encoding => 'UTF-8')

In Ruby 1.9, an alternative syntax for a limited subset of Hash literals was introduced: when the key is a Symbol that is also a valid Ruby identifier (e.g. :foo, but not :'foo-bar'), then you can write it like this:

{ foo: bar }

instead of

{ :foo => bar }

So, we could call our method from above like this:

open_file('test.txt', { mode: 'r', encoding: 'UTF-8' })

and, since the rule about leaving out parentheses still applies, also like this:

open_file('test.txt', mode: 'r', encoding: 'UTF-8')

This looks very much like keyword arguments in other languages. In fact, this alternative literal syntax for Hashes with Symbol keys was at least partially specifically designed to provide a transition path for introducing keyword parameters and arguments into Ruby.

In Ruby 2.0, optional keyword parameters with default keyword arguments were introduced:

def open_file(name, mode: 'w', eol: :crlf, encoding: nil)
  raise ArgumentError unless encoding
  # do your thing
end

Then in Ruby 2.1 mandatory keyword parameters and arguments:

def open_file(name, mode: 'w', eol: :crlf, encoding:)
  # do your thing
end

As you probably know, calling this method looks exactly like it did before:

open_file('test.txt', mode: 'r', encoding: 'UTF-8')

Note, however, that you can no longer tell what this means! You cannot know whether mode: 'r', encoding: 'UTF-8' is a Hash literal or two keyword arguments (in other words, you don't even know whether this is one or two arguments!) without looking at the definition of the method you are calling.

It was decided that Ruby 2.0 should be maximally backwards and forwards compatible with Ruby 1.9.

Therefore, all of the following must be true:

  • A method that is defined with an options hash and is called with an options hash must still work.
  • A method that is defined with an options hash and is called with keyword arguments must still work.
  • A method that is defined with keyword parameters and is called with an options hash literal must still work.

In order to make all of this work, there are a lot of implicit conversions between hash literals and keyword arguments. Getting this to work without nasty corner cases is just much easier if keyword parameters and keyword arguments are only allowed to appear where the "fake" keyword syntax was allowed before, i.e. at the very end of the parameter list and argument list.

Actually, there are still a lot of nasty corner cases caused by this "blurring the lines" between hashes and keyword parameters. If you look through the Ruby issue tracker, you will find that a significant portion of issues reported since Ruby 2.0 are related to un-intuitive or simply buggy behavior in this regard. Every new release brings new changes, but one gets the feeling that for every hole they patch, they create two new ones.

Now, just imagine what it would be like if the rules were even less strict!


Here are some examples of those afore-mentioned issues:

  • Unexpect behavior when using keyword arguments
  • Can't pass hash to first positional argument; hash interpreted as keyword arguments
  • Keyword argument oddities
  • Splat with empty keyword args gives unexpected results
  • inconsistent behavior using ** vs hash as method parameter
  • Mixing kwargs with optional parameters changes way method parameters are parsed
  • Procs keyword arguments affect value of previous argument
  • Optional argument treated as kwarg
  • Object converted to Hash unexpectedly under certain method call
  • Default Parameters don't work
  • Some weird behaviour with keyword arguments
  • Keyword arguments are ripped from the middle of hash if argument have default value
  • must not optimize foo(**{}) out
  • non-symbol keyword in double splat, such as **{2 => 3}, raises TypeError or not
like image 175
Jörg W Mittag Avatar answered Feb 27 '26 16:02

Jörg W Mittag



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!