Project

General

Profile

Actions

Feature #17808

open

Feature Request: JS like splat of Object properties as named method parameters

Added by Lithium (Brad Krane) 5 months ago. Updated 5 months ago.

Status:
Open
Priority:
Normal
Assignee:
-
Target version:
-
[ruby-core:103481]

Description

I'm pretty sure there is no equivalent Ruby for a very convenient JS way to enter named function parameters as below:

const test = ({param1,
 param2,
 param3 = null,
 param4 = false,
 param5 = null,
 }={}) => {
   console.log(`${param1}, ${param2}, ${param3}, ${param4}, ${param5}\n`)    
 }
let obj = {
 param1: 23,
 param2: 234,
 param3: 235,
 param4: 257
};

test({...obj});

which is super convenient and as far as I'm aware there is no standard Ruby equivalent. It can be accomplished in Ruby but the call syntax is far less nice. A couple examples below:

# Implementing such a feature wouldn't be too difficult.
# Ok so here is what you could do it. Patch Kernel with a method splat. (Sorry in advance for formatting)
module Kernel
  def splat obj, method:
    new_hash = method.parameters.reduce({}) do |hash, attrr|
      hash[attrr.last] = obj.send(attrr.last)
      hash
    end
  end
end

# Then you can pass it a list of symbols.
# Then for your method:
def some_method name:, weight: 
    puts "#{name} weighs #{weight}"
end

class Dog
  attr_reader :name, :weight
  def initialize name:,weight: 
    @name = name
    @weight = weight
  end
end

a_dog = Dog.new( name: 'fido', weight: '7kg')
hash_puppy = a_dog.splat(a_dog, method: method(:some_method)  )

some_method(**hash_puppy)

or what I think is a bit better:

# Same class as above
a_dog = Dog.new( name: 'fido', weight: '7kg')

def other_splat  obj, method:
  new_hash = method.parameters.reduce({}) do |hash, attrr|
    if obj.class <= Hash
      hash[attrr.last] = obj[attrr.last]      
    else
      hash[attrr.last] = obj.send attrr.last
    end
    hash
  end
  method.call **new_hash
end

other_splat(a_dog, method: method(:some_method))

# above line should be like:
# some_method ...a_dog

Source: https://gist.github.com/bradkrane/e051d205024a5313cb4a5b9eb1eae0e3

I'm sure I'm missing a possibly more clever way to accomplish this, but I'm pretty sure something like other_splat(a_dog, method: method(:some_method)) is about as close as it can get, unless I'm missing something? It would be quite nice to have a similar syntax as JS but in Ruby: some_method ...a_dog

Thanks for your time and attention!

Updated by byroot (Jean Boussier) 5 months ago

Doesn't the implicit to_hash answer your demand?

def foo(bar:)
  p bar
end

class MyObject
  def initialize(bar)
    @bar = bar
  end

  def to_hash
    { bar: @bar }
  end
end

foo(**MyObject.new(42)) # => 42

Updated by Lithium (Brad Krane) 5 months ago

Thanks for the quick update.

That almost works but to automate this the to_hash function needs to be aware of the methods named arguments. The below works in one case and fails in another with an object having more instance variables than the called method and it throws a [ArgumentError] unknown keyword: :color as there is now an extra parameter included that is not one of the named method params.

Also would this be dangerous to begin with modifying the Kernel to_hash?

module Kernel
  def to_hash
    new_hash = self.instance_variables.reduce({}) do |hash, instance_var|
      hash[instance_var.to_s[1..-1].to_sym] = self.instance_variable_get instance_var
      hash
    end
  end
end

# Then you can pass it a list of symbols.
# Then for your method:
def some_method name:, weight: 
    puts "#{name} weighs #{weight}"
end

class Dog
  attr_reader :name, :weight
  def initialize name:,weight: 
    @name = name
    @weight = weight
  end
end

a_dog = Dog.new name: 'fido', weight: '7kg'

some_method(**a_dog)  # Works great!

class CockerSpanel < Dog
  def initialize name:,weight:,color:
    super name: name, weight: weight
    @color = color
  end
end

another_dog = CockerSpanel.new name: 'Jessie', weight: '5kg', color: 'black'

some_method **another_dog

NVM on the hash question it is a problem pasting into irb crashes floods console with the below and needs a ctrl+C or 10 to terminate. It is quite colorful though :D


C:\Users\Brad Krane>ruby -v
ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x64-mingw32]
C:/Ruby27-x64/lib/ruby/2.7.0/reline/unicode.rb:70: warning: Using the last argument as keyword parameters is deprecated
C:/Ruby27-x64/lib/ruby/2.7.0/reline/unicode.rb:70: warning: Using the last argument as keyword parameters is deprecated
C:/Ruby27-x64/lib/ruby/2.7.0/reline/unicode.rb:70: warning: Using the last argument as keyword parameters is deprecated
C:/Ruby27-x64/lib/ruby/2.7.0/reline/unicode.rb:70: warning: Using the last argument as keyword parameters is deprecated
C:/Ruby27-x64/lib/ruby/2.7.0/reline/unicode.rb:70: warning: Using the last argument as keyword parameters is deprecated
C:/Ruby27-x64/lib/ruby/2.7.0/reline/line_editor.rb:1080: warning: Using the last argument as keyword parameters is deprecated
C:/Ruby27-x64/lib/ruby/2.7.0/reline/line_editor.rb:1063: warning: Using the last argument as keyword parameters is deprecated
C:/Ruby27-x64/lib/ruby/2.7.0/reline/unicode.rb:70: warning: Using the last argument as keyword parameters is deprecated
C:/Ruby27-x64/lib/ruby/2.7.0/reline/unicode.rb:70: warning: Using the last argument as keyword parameters is deprecated
C:/Ruby27-x64/lib/ruby/2.7.0/reline/unicode.rb:70: warning: Using the last argument as keyword parameters is deprecated
C:/Ruby27-x64/lib/ruby/2.7.0/reline/unicode.rb:70: warning: Using the last argument as keyword parameters is deprecated
C:/Ruby27-x64/lib/ruby/2.7.0/reline/unicode.rb:70: warning: Using the last argument as keyword parameters is deprecated
C:/Ruby27-x64/lib/ruby/2.7.0/reline/unicode.rb:70: warning: Using the last argument as keyword parameters is deprecated
C:/Ruby27-x64/lib/ruby/2.7.0/irb/ext/save-history.rb:110: warning: Using the last argument as keyword parameters is deprecated
Traceback (most recent call last):
        13096: from C:/Ruby27-x64/bin/irb.cmd:31:in `<main>'
        13095: from C:/Ruby27-x64/bin/irb.cmd:31:in `load'
        13094: from C:/Ruby27-x64/lib/ruby/gems/2.7.0/gems/irb-1.2.3/exe/irb:11:in `<top (required)>'
        13093: from C:/Ruby27-x64/lib/ruby/2.7.0/irb.rb:399:in `start'
        13092: from C:/Ruby27-x64/lib/ruby/2.7.0/irb.rb:470:in `run'
        13091: from C:/Ruby27-x64/lib/ruby/2.7.0/irb.rb:470:in `catch'
        13090: from C:/Ruby27-x64/lib/ruby/2.7.0/irb.rb:471:in `block in run'
        13089: from C:/Ruby27-x64/lib/ruby/2.7.0/irb.rb:536:in `eval_input'
         ... 13084 levels...
            4: from C:/Ruby27-x64/lib/ruby/2.7.0/reline/line_editor.rb:93:in `block in reset'
            3: from C:/Ruby27-x64/lib/ruby/2.7.0/reline/line_editor.rb:93:in `block in reset'
            2: from C:/Ruby27-x64/lib/ruby/2.7.0/reline/line_editor.rb:93:in `block in reset'
            1: from C:/Ruby27-x64/lib/ruby/2.7.0/reline/line_editor.rb:93:in `block in reset'
C:/Ruby27-x64/lib/ruby/2.7.0/reline/line_editor.rb:93:in `block in reset': stack level too deep (SystemStackError)

Running the file itself we get

C:\Users\Brad Krane\Documents\src\ruby-splat>ruby ruby-splat.rb
fido weighs 7kg
Traceback (most recent call last):
        1: from james.rb:38:in `<main>'
ruby-splat.rb:13:in `some_method': unknown keyword: :color (ArgumentError)

C:\Users\Brad Krane\Documents\src\ruby-splat>

How could I automate the to_hash knowing the method arguments required so as to prevent the unknown keyword: :color (ArgumentError) Or should I be specifying my to_hash methods individually and if I did how can I specify the correct parameters?

Updated by byroot (Jean Boussier) 5 months ago

Well, the ruby equivalent to your example JS function would be:

def test(param1: nil, param2: nil, **)
end 

The ** will catch any key that don't match any of the declared keyword arguments.

Updated by Lithium (Brad Krane) 5 months ago

byroot (Jean Boussier) wrote in #note-3:

Well, the ruby equivalent to your example JS function would be:

def test(param1: nil, param2: nil, **)
end 

The ** will catch any key that don't match any of the declared keyword arguments.

This does work, but this requires modifying all methods to include this extra parameter to avoid the error.

Is there a way to modify existing declared methods adding that parameter so as to avoid an error when additional params are provided in error, due to the object having more instance_variables for whatever reason?

I can't see how to escape the following syntax for this functionality without heavy lifting: other_splat(a_dog, method: method(:some_method))

With the Kernel.to_hash monkey patching how can one avoid the argument error for 'poorly written methods' that don't have the extra ** already?

There is also the case an object is passed to many different methods all with different named params selectively grabbing what's needed for the moment, without having to mod all the methods to have a **, or specifying a specific to_hash_some_method for each method, or using other_splat(a_dog, method: method(:some_method)) which does work but is ugly af for Ruby in my opinion.

Updated by byroot (Jean Boussier) 5 months ago

Is there a way to modify existing declared methods adding that parameter

Well, I suppose you can decorate the methods and filter the extra keywords, a bit tricky but doable:

class Module
  def ignore_extra_parameters(method_name)
    method = instance_method(method_name)
    keyword_args = method.parameters.filter_map { |type, name| name if type == :key || type == :keyreq }
    define_method(method_name) do |*args, **kwargs, &block|
      method.bind_call(self, *args, **kwargs.slice(*keyword_args), &block)
    end
  end
end

class MyObject
  def initialize(bar)
    @bar = bar
  end

  def to_hash
    { bar: @bar, plop: 42 }
  end
end

module Test
  extend self
  ignore_extra_parameters def foo(bar:)
    p bar
  end
end

Test.foo(**MyObject.new(42)) # => 42

But ultimately your whole feature request rely on a very specific JS "feature" (many people would call it a cruft), that extra arguments are simply ignored.

That's not the case in Ruby, every argument must be accounted for. And while I'm merely an observer of the ruby-core development, I'm pretty sure it isn't acceptable to change. So I doubt there any way such feature would be implemented.

Or you'd need to introduce some new syntax for the caller to specify that the extra parameters should be discarded, but again, not very likely to happen.

Updated by marcandre (Marc-Andre Lafortune) 5 months ago

I believe the answers given so far should help (i.e. (**rest) and defining to_hash).

Also looks into Struct that does some of that for you. I wish Struct was better for inheritance though, I'd like to propose a better solution one day.

In the meantime, if this discussion needs to continue, it would be more appropriate on StackOverflow / some code-refactoring site, as there doesn't seem to be a particular feature request.

Updated by Lithium (Brad Krane) 5 months ago

But ultimately your whole feature request rely on a very specific JS "feature" (many people would call it a cruft), that extra arguments are simply ignored.

Yes likely but it is very convenient.

That's not the case in Ruby, every argument must be accounted for. And while I'm merely an observer of the ruby-core development, I'm pretty sure it isn't acceptable to change. So I doubt there any way such feature would be implemented.

Or you'd need to introduce some new syntax for the caller to specify that the extra parameters should be discarded, but again, not very likely to happen.

I agree with you here. I suppose the ultimate question is if I can't redefine to_hash for everything, verified bad idea, and I don't want to mod all methods like in your ignore_extra_parameters example, (which is brilliant btw), how can I call my to_hash method with it knowing if it is a ** reduction and if so what caller is so as to be able to yield the appropriate hash for this to work?

Can a methods parameter identify the function it's in before it's called? and then be able to define itself based on the named params of the method after called into?

For this to work a parameter needs to know it's calling method and then react to it appropriately.

some_method splat(object) is the ideal syntax but seems impossible and where

where some_method splat(object) is really some_method splat(object, method: method(some_method))

so you could detect and emit the correct params as opposed to spamming them.

Towards Marc-Andre

I will have to study the Struct class as this looks promising but I haven't the time atm.

(edit) I like this alot and I would love to see it working like a cluge JS when passed to a func/method with named params 'splatting' them so to speak or otherwise behaving like a JS object which this appears to do quite nicely form the bits I've seen

Actions

Also available in: Atom PDF