Issue #16411 has been updated by Dan0042 (Daniel DeLorme).


I'm aware of `**nil` and it's true that it provides fully safe keyword extension. But you have to admit it's not likely to become common practice amongst rubyists. You'd have to add it to every regular method in advance, just in case it might eventually accept keyword arguments; that's just too much boilerplate.

---

Thinking about it some more I think I reached something interesting:

```ruby
class Module
  def safe_keyword_extension(**defined)
    name = defined.delete(:for) or raise ArgumentError, "must specify method with 'for'"
    original = instance_method(name)
    define_method(name) do |*args, **keywords, &b|
      unless keywords.all?{ |k,v| t = defined[k] and t === v }
        args << keywords
        keywords = {}
      end
      original.bind(self).call(*args, **keywords, &b)
    end
  end
end

module Kernel
  safe_keyword_extension output: IO, for:
  def debug(*args, output: $stdout)
    args.each {|arg| output.puts arg.inspect }
  end
end

debug(key: 42)                                   #=> {:key=>42}
debug({key: 42}, output: STDOUT)                 #=> {:key=>42}
debug({input: "a", output: "A"})                 #=> {:input=>"a", :output=>"A"}
debug(input: "a", output: "A")                   #=> {:input=>"a", :output=>"A"}
debug({input: "a", output: "A"}, output: STDERR) #=> {:input=>"a", :output=>"A"}
debug(output: "A")                               #=> {:output=>"A"}
```

By validating that the keywords given to the `debug` method are all part of the defined keywords AND of the correct type, we can even make it work for `debug(output: "A")` which I previously thought impossible.

----------------------------------------
Feature #16411: Safer keyword argument extension
https://bugs.ruby-lang.org/issues/16411#change-83060

* Author: Dan0042 (Daniel DeLorme)
* Status: Open
* Priority: Normal
* Assignee: 
* Target version: 
----------------------------------------
The problem of safe keyword extension has been [described like this](https://bugs.ruby-lang.org/issues/14183#note-29):

```ruby
#We have an existing method
def debug(*args)
  args.each {|arg| puts arg.inspect }
end

#It can be invoked using the "braceless hash style"
debug(key: 42) #=> {:key=>42}

#Then, consider we improve (extend) the method to accept the output IO as a keyword parameter "output":
def debug(*args, output: $stdout)
  args.each {|arg| output.puts arg.inspect }
end

#However, this change breaks the existing call.
debug(key: 42) #=> ArgumentError (unknown keyword: key)
```

The best solution to this is currently seen as

```ruby
def debug(*args, output: $stdout, **hash)
  args << hash unless hash.empty?
  args.each {|arg| output.puts arg.inspect }
end
debug(key: 42) #=> {:key=>42}
```

With the caveat that it doesn't work if the braceless hash has a key in common with the new keyword argument(s):

```ruby
debug(input: "a", output: "A") #=> NoMethodError (private method `puts' called for "A":String)
```

That's a reasonable compromise, but I think it would be **very** unusual to mix positional arguments and keyword arguments into a single braceless hash. So in the case above, if `hash` is non-empty, it's quite likely that the `output` key should be part of it, and not interpreted as a keyword argument. In short I'm saying that:

```ruby
debug({input: "a"})                #is likely
debug(input: "a")                  #is likely
debug({input: "a"}, output:STDERR) #is likely
debug(input: "a", output:STDERR)   #is unlikely, or at least very bad form
```

So following from that, I believe the safest way to extend a method with keyword arguments is like this:

```ruby
def debug(*args, **hash)
  defaults = {output: $stdout}
  kw = defaults.merge(hash)
  if kw.size > defaults.size #hash has some key not in defaults
    args << hash
    kw = defaults
  end
  output = kw[:output]
  args.each {|arg| output.puts arg.inspect }
end
debug(key: 42)                                   #=> {:key=>42}
debug({input: "a", output: "A"})                 #=> {:input=>"a", :output=>"A"}
debug(input: "a", output: "A")                   #=> {:input=>"a", :output=>"A"}
debug({input: "a", output: "A"}, output: STDERR) #=> {:input=>"a", :output=>"A"}
```

Of course it doesn't handle `debug(output: "A")` but I don't think there's any generic way to handle that.

The solution above is obviously way too verbose and way too much boilerplate to be used as-is, and that's why I'd like ruby to provide some kind of facility to perform this operation. The above could be somewhat simplified via some kind of utility function like

```ruby
def debug(*args, **hash)
  kw = kw_or_hash(hash, output: $output){ |hash| args << hash }
  output = kw[:output]
  args.each {|arg| output.puts arg.inspect }
end
```

but ideally this safe keyword extension mechanism would allow to keep the keywords definition inside the method definition, unlike the above.
This could perhaps be done via some special syntax like

```ruby
def debug(*args, output: $stdout, **!?)
```

but I don't think yet more special-case syntax is an ideal solution either.
Maybe the ideal would be via meta-programming like 

```ruby
extended_keywords def debug(*args, output: $stdout)
```
 but I'm not sure how that would work internally. Is it possible?



-- 
https://bugs.ruby-lang.org/

Unsubscribe: <mailto:ruby-core-request / ruby-lang.org?subject=unsubscribe>
<http://lists.ruby-lang.org/cgi-bin/mailman/options/ruby-core>