Ruby method with a (*) signature

Ruby method with a (*) signature


9

On this interesting blog post about pattern matching, there is some code with a method signature of (*)

class Request < Data.define(:path, :scheme, :format)
  def deconstruct_keys(*)
    { path: @path, scheme: @scheme, format: @format }
  end

  def deconstruct(*)
    path.split("/").compact
  end
end

This is different than

def a_method(*args)

I could not find any information in the Ruby docs.

What does def deconstruct_keys(*) mean?

6

  • More accurately, the (*) is the parameter list, not the signature…and I'm also curious what this is.

    – Code-Apprentice

    yesterday

  • Googling "ruby * parameter" leads me to a bunch of hits that say this is the exact same as *args, just that it is anonymous.

    – Code-Apprentice

    yesterday

  • 3

    It's similar to the _ "name" for "scalars". 3.2 changelog calls them "anonymous rest and keyword rest arguments": docs.ruby-lang.org/en/3.2/NEWS_md.html#label-Language+changes

    – jqurious

    yesterday

  • It's a more limited version of ... that allows you to forward/delegate any number of parameters to another method, or to ignore them as your examp,e does. The differences between *, **, and ... are potentially confusing, but in this case it just ignores all parameters passed as if you had used _ for each passed argument.

    – Todd A. Jacobs

    yesterday


  • 1

    The previous close target was not an exact duplicate. While similar, thre are enough differences to keep this one open. In addition, the accepted answer there was not dispositive, referring to a 13 year old Ruby version with different internal implentations and semantics.

    – Todd A. Jacobs

    22 hours ago

2 Answers
2


20

def a_method(*args)
  ...
end

Normally, when we write this, we get a list of all of the method’s arguments stored in the args variable.

def a_method(*)
  ...
end

This is an anonymous form of the same. We accept any number of arguments but don’t wish to give a name to that list variable. Now, we haven’t given the list of arguments a name, but we can still splat it into another argument list. So while this isn’t legal

def a_method(*)
  # Should've just named it in the first place :(
  args = *
  ...
end

this, on the other hand, is

def a_method(*)
  another_method(*)
end

and will pass the arguments onto another_method. It’s equivalent to

def a_method(*args)
  another_method(*args)
end

The same can be done with Ruby 3 keyword arguments

def a_method(**)
  another_method(**)
end

Note that, if your intention is to forward all arguments, you should use the ellipsis syntax.

def a_method(...)
  another_method(...)
end

A lone * will act funny when delegating keyword arguments. For instance,

def foo(*args, **kwargs)
  p args
  p kwargs
end

def bar(*)
  foo(*)
end

foo(1, a: 1) # Prints [1] then {:a=>1}
bar(1, a: 1) # Prints [1, {:a=>1}], then {}

When calling foo directly, named argument syntax gets passed to **kwargs, but when delegating through bar, it gets converted into a hash and then passed into *args. On top of that, * won’t forward block arguments either, whereas ... is your general-purpose "pass all positional, named, and block arguments onward" catch-all.

2

  • This would be a better answer if it also addressed the memory and speed benefits even when not forwarding, especially in Ruby 3.2+.

    – Todd A. Jacobs

    22 hours ago


  • 1

    @user3840170 Oops! I removed that sentence, as it looks like I was misremembering (though, in my mind, that's Python's fault; that bare * really does look like it should be taking arbitrary positional arguments to my eyes). Thanks for pointing it out!

    – Silvio Mayolo

    4 hours ago


7

Used for Delegation, Forwarding, or Ignoring Arguments

In Brief

This syntax has been around since Ruby 2.7, but has gained additional powers and some related tokens on the way to Ruby 3.2. Briefly, it’s a more limited version of ... that allows you to forward/delegate any number of parameters to another method without having to allocate them or deconstruct them first as you did in earlier versions of Ruby. It also allows you to ignore any arguments, which is what your example is doing.

Deeper Explanation

The differences between *, **, and ... are potentially confusing, but in the posted code it just ignores any and all parameters passed just as if you had manually assigned each argument to the throwaway _, or collected the positional, keyword, and block arguments without using any of them in the method body.

Aside from saving some typing, this syntax avoids useless arity checks and wasteful memory allocations when forwarding or ignoring arguments. Ruby’s internals have been optimized for this, so essentially a method like:

def deconstruct_keys(*)
  { path: @path, scheme: @scheme, format: @format }
end

will take any number of arguments, or none at all, in a more memory-efficient way. It also avoids the need to worry about what the caller may or may not pass if you’re just forwarding or ignoring the arguments to this method.

In your posted example, the return value from the method is a Hash of instance variable names and values that have nothing to do with any parameters that might or might not be passed to the method. Those variables are defined by the Data object which is the ancestor class, and the values are likely set elsewhere in unposted code. (Side note: unset instance variables auto-vivify as nil so would not raise an exception here even if unset.) However, for the purposes of explaining your example, the essential point is that the return value of the method is not related in any way to the #deconstruct_keys method signature as it currently ignores any arguments.



Leave a Reply

Your email address will not be published. Required fields are marked *