2

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?

kingsfoil
  • 3,795
  • 7
  • 32
  • 56
  • You may find [this doc](https://docs.ruby-lang.org/en/2.5.0/syntax/calling_methods_rdoc.html) and [this video](https://thoughtbot.com/upcase/videos/ruby-keyword-arguments) useful, though I understand that your question is about requirements for the ordering of named arguments relative to positional arguments. – Cary Swoveland Dec 28 '18 at 07:00

2 Answers2

12

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:

Jörg W Mittag
  • 363,080
  • 75
  • 446
  • 653
0

You'd probably have to dig around ruby-forums to find an authoritative answer, but I suspect this is done to enforce readability with the ability to omit the braces around the hash when calling the method.

Consider a method like this:

def foo(num, hash, string); end

This is a valid invocation:

foo(1, {a: :b}, "bar")

This is not, because the parser doesn't know whether the comma separates hash key vals or arguments to the method:

foo(1, a: b, "bar")

Certainly the method would be more flexible if you put the hash as the last argument instead, because then you'd have the option to omit the braces around the hash.

You could make the argument that the parser should be able to know what that last comma means, but I can't tell you how difficult that is because I don't know the internals of the Ruby parser.

It would be great if we had more full-featured destructuring like in ES6 Javascript, but alas we don't (again, how much a challenge this would be to implement, I can't tell you). What we do have is a kind of rudimentary hash destructuring in the form of keyword params. And, the likely reason the keyword params have to be last, is to ensure that the syntactic sugar where we omit the hash braces is available.

max pleaner
  • 26,189
  • 9
  • 66
  • 118