Calling Methods and Argument Forwarding

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Ruby 4.0 (4.0.7, checked on 2026-10-18). Every script example was run with ruby -w on Ruby 4.0.7 without a warning, passes ruby -wc and is clean under RuboCop 1.92 with the configuration of the running shelf gem (double-quoted strings, no class-documentation cop), apart from the one deliberate offence noted below; the output shown is the real output, and the shell sessions show the exact commands that produced the warnings and syntax errors. Features newer than Ruby 3.0 name the version that introduced them.

Calling a method is sending a message to an object. Most of the time that is as simple as book.title, but the argument list hides a good deal of machinery: arguments are positional, keyword or a block; each kind can be given literally or unpacked from a collection with *, ** or &; and since Ruby 2.7 a method can hand its whole argument list to another method without naming any of it. This page is the calling side. The defining side (parameter kinds, defaults, required and optional keywords, **nil, Method#parameters) is on Methods and Parameters; blocks, procs and lambdas as objects are on Blocks, Procs and Lambdas; how Ruby decides which method runs is on Method Lookup and super; calling a method whose name is only known at run time, and defining methods on the fly, is on Dynamic Dispatch and Method Definition.

The examples use the small shelf library catalogue (a Book value object, a Catalog, a Store) that runs through the whole Ruby reference. Each example is a complete script, so the # frozen_string_literal: true magic comment (see Program Structure and Lexical Elements) appears at the top.

Receivers and implicit self

Every method call has a receiver, the object that gets the message. With an explicit receiver (book.title) the receiver is the expression before the dot. Without one (puts "x", render) the receiver is self, the current object, which is the top-level main object in a script and the instance inside an instance method. The documentation of the call syntax states it plainly: self is the default receiver.

# frozen_string_literal: true

module Shelf
  Book = Data.define(:title, :author)

  # Prints a one-line card for a book and counts the renders.
  class Card
    def initialize(book)
      @book = book
      @renders = 0
    end

    def render
      self.renders = renders + 1
      "#{heading} (#{byline}) ##{renders}"
    end

    private

    attr_accessor :renders

    def heading = @book.title.upcase

    def byline = @book.author
  end
end

book = Shelf::Book.new(title: "Rebuilding the Shelf", author: "Ada")
card = Shelf::Card.new(book)

puts card.render
puts card.render
puts book.title.upcase.center(24, "*")

begin
  card.heading
rescue NoMethodError => e
  puts e.message
end

begin
  card.renders
rescue NoMethodError => e
  puts e.message
end

begin
  nil.upcase
rescue NoMethodError => e
  puts e.message
end
REBUILDING THE SHELF (Ada) #1
REBUILDING THE SHELF (Ada) #2
**REBUILDING THE SHELF**
private method 'heading' called for an instance of Shelf::Card
private method 'renders' called for an instance of Shelf::Card
undefined method 'upcase' for nil

Three things to notice in the example:

  • render calls heading and byline with the implicit receiver. They are private, which only forbids calling them with an explicit receiver; card.heading from outside raises NoMethodError with the message private method 'heading' called for an instance of Shelf::Card (the receiver’s class in the message is Ruby 3.3+ and the straight quotes are Ruby 3.4+; older versions printed a backtick and the receiver’s inspect).

  • self.renders = renders + 1 writes through a private writer with an explicit self receiver. For a setter this is required (a bare renders = 1 assigns a local variable), and Ruby 2.7 relaxed the rule so that any private method may be called as self.name. The reader renders is called with the implicit receiver. See Visibility and Encapsulation for the full rules.

  • the last case calls upcase on nil. A nil receiver is the commonest source of NoMethodError; the message names the receiver as nil, not as NilClass (the undefined method 'x' for nil:NilClass wording of the books is from before Ruby 3.3).

Besides the dot there are a few other ways to write a receiver and a call:

Form Meaning

book.title

The usual call, with an explicit receiver.

title

Implicit receiver self. If title is a local variable in scope, that wins; otherwise it is a method call.

self.title

Explicit self. Needed for setters (self.title = x); allowed for private methods since Ruby 2.7.

Shelf::Card.new(book)

The :: after a constant is a namespace lookup; Shelf::Card is a constant, and .new the call.

Kernel::format("%d", 3)

:: before a lowercase name is also a method call. It works but is rarely used because it is easy to confuse with a namespace; RuboCop reports it as Style/ColonMethodCall.

callable.(1)

Syntax sugar for callable.call(1) on any object. RuboCop’s default Style/LambdaCall prefers the explicit .call.

callable[1]

Calls [], which Proc and Method define as an alias of call; Hash#[] and Array#[] are the same syntax with different meanings.

book&.title

Safe navigation: calls the method only if the receiver is not nil (see below).

A call that finds no method on the receiver ends in method_missing and then NoMethodError (NameError for a bare identifier that could have been a local variable: undefined local variable or method 'x' for main). The lookup order is on Method Lookup and super.

Parentheses

Parentheses around the arguments are optional in Ruby. puts "hi", puts("hi") and Integer "7" are all legal. The documentation uses parentheses whenever arguments are present, except where it is demonstrating the difference, and the same is the practice for library code: omit them for a zero-argument call (book.title), for statement-like calls that read as keywords (puts, raise, require, attr_reader, private) and in DSLs, and write them everywhere else. The rules that make this more than style:

  • A space before the parenthesis changes the meaning. puts (1 + 2) * 3 passes the whole expression (1 + 2) * 3 as the argument; puts(1 + 2) * 3 calls puts with 3 and then multiplies its nil result by 3.

  • Nested command calls need parentheses. b 1, a 2, 3 is a SyntaxError because Ruby cannot tell which call the last argument belongs to. Parenthesize the inner call: b(1, a(2), 3).

  • puts {} is a block, not an empty hash. An opening brace right after a method name starts a block. Use puts({}) or puts(Hash.new) to print a hash.

  • A first argument that starts like an operator is ambiguous. puts -1, p x -1 (with x a local variable) and show *list all invite a reading as a binary operator, and Ruby warns in verbose mode.

$ ruby -w -e 'puts (1 + 2) * 3'
9
$ ruby -w -e 'puts(1 + 2) * 3'
-e:1: warning: possibly useless use of * in void context
3
-e:1:in '<main>': undefined method '*' for nil (NoMethodError)
$ ruby -w -e 'puts {}.class'

$ ruby -w -e 'puts({}.class)'
Hash
$ ruby -w -e 'puts -1'
-e:1: warning: ambiguous first argument; put parentheses or a space even after `-` operator
-1

The prefix warnings (the word "ambiguous", the operator in backticks, then "has been interpreted as an argument prefix") are the ones the call syntax guide describes for a splat, double splat or block conversion that comes first in a call without parentheses. They name the line and are cheap to avoid:

$ ruby -w -e 'def show(*list) = p(list)
nums = [1, 2]
show *nums'
-e:3: warning: ambiguous `*` has been interpreted as an argument prefix
[1, 2]
$ ruby -w -e 'def show(**opts) = p(opts)
opts = { a: 1 }
show **opts'
-e:3: warning: ambiguous `**` has been interpreted as an argument prefix
{a: 1}
$ ruby -w -e 'def twice(&block) = block.call * 2
blk = -> { 21 }
p twice &blk'
-e:3: warning: ambiguous `&` has been interpreted as an argument prefix
42
$ ruby -w -e 'def show(*list) = p(list)
nums = [1, 2]
show(*nums)'
[1, 2]

A call whose last argument is followed by a newline ends there. To continue a call on the next line, leave a trailing comma or an open bracket, or start the next line with a leading dot (see the next section).

Chaining calls and the safe navigation operator

A call chain is one call followed immediately by another: books.select(&:author).map(&:title).sort. When the chain is long, put each call on its own line starting with the dot (a "leading dot" chain); the newline before a . does not end the expression. Ruby 4.0 extends the same idea to logical operators: a line that starts with && or || (or with the keyword forms and and or) continues the previous line.

The safe navigation operator &. calls the method only when the receiver is not nil; otherwise the call is skipped, its arguments are not evaluated, and the result is nil. It protects exactly one call, so a longer chain needs the operator at every step that can see nil. Only nil short-circuits: false is a real receiver.

# frozen_string_literal: true

module Shelf
  Author = Data.define(:name)
  Book = Data.define(:title, :author)
end

books = [
  Shelf::Book.new("Quiet Streets", Shelf::Author.new("Lin")),
  Shelf::Book.new("Loud Roofs", Shelf::Author.new("Ada")),
  Shelf::Book.new("Hidden Rooms", nil)
]

# Leading-dot chain: each line continues the expression above it.
titles = books
         .select(&:author)
         .map { it.title.downcase }
         .sort
p titles

book = books.last

# A leading logical operator continues the previous line (Ruby 4.0).
publishable = book.title.length > 5
              && !book.author.nil?
p publishable

# Without &. the nil author blows up.
begin
  book.author.name
rescue NoMethodError => e
  puts e.message
end

# &. skips exactly one call.
p book.author&.name
p book.author&.name.nil?
p book.author&.name&.upcase

# Skipped calls do not evaluate their arguments.
count = 0
p book.author&.name&.center(count += 1)
p count

# Only nil short-circuits; false does not.
p([false, nil].map { it&.to_s })
p books.first.author&.name&.upcase
["loud roofs", "quiet streets"]
false
undefined method 'name' for nil
nil
true
nil
nil
0
["false", nil]
"LIN"

Details worth remembering:

  • book.author&.name.nil? is true for a book without an author, because after the skipped call the .nil? runs on the nil result: &. does not skip the rest of the chain. To end a chain safely, repeat &. or use a default: book.author&.name || "unknown". RuboCop reports the pattern as Lint/SafeNavigationChain.

  • &. is also valid on the left of an assignment (book.author&.name = "x", which does nothing when the author is nil) and with operator assignment (a&.count += 1).

  • &. hides where the nil came from. Use it where nil is a documented possibility, not to silence a bug; the alternatives are Hash#fetch, a case/in pattern (see Pattern Matching) or a null object.

  • The Ruby 4.0 leading && is new: older Rubies parse a line starting with && as a syntax error, so code that must run on Ruby 3.4 or earlier keeps the operator at the end of the previous line.

The shape of an argument list

A call can have three kinds of argument, and they must appear in this order: positional arguments, then keyword (or hash) arguments, then the block. Each is optional.

Kind Call-site syntax Notes

Positional

f(a, b), f(*list)

Matched to parameters left to right.

Keyword

f(key: value), f(key:), f(**hash)

Matched by name. Written as a braceless list of key: value pairs.

Hash (braced)

f({ key: value })

One ordinary positional argument that happens to be a Hash.

Block

f { ... }, f do ... end, f(&proc)

Always last; not part of the positional count.

Arguments are passed by reference to the object (callers and callee share the same object, as with every assignment in Ruby, so mutating a string inside a method is visible outside) and they are not lazy: all argument expressions are evaluated, left to right, before the call.

# frozen_string_literal: true

module Shelf
  def self.rename!(title, suffix) = title << suffix

  def self.renamed(title, suffix) = "#{title}#{suffix}"

  # Arguments are evaluated eagerly, left to right, before the call.
  def self.pair(left, right) = [left, right]
end

title = +"Quiet Streets"
same = Shelf.rename!(title, " (2nd ed.)")
puts title
puts same.equal?(title)

literal = "Loud Roofs"
begin
  Shelf.rename!(literal, " (2nd ed.)")
rescue FrozenError => e
  puts e.message
end
puts Shelf.renamed(literal, " (2nd ed.)")

order = []
Shelf.pair(order.push(:left), order.push(:right))
p order

# A default expression runs at call time and cannot see the caller's locals.
def stamp(entry, at = order.length) = [entry, at]
begin
  stamp("x")
rescue NameError => e
  puts e.message
end
Quiet Streets (2nd ed.)
true
can't modify frozen String: "Loud Roofs"
Loud Roofs (2nd ed.)
[:left, :right]
undefined local variable or method 'order' for main

The FrozenError is the consequence of # frozen_string_literal: true (see Strings); +"..." or dup gives a mutable copy. A method that must not change its input should not use << on it; build a new string instead, as renamed does. See Object Lifecycle, Freezing and Copying for the details.

Positional arguments

Positional arguments follow the method name and are matched to the required, optional and rest parameters in order. Too few or too many raise ArgumentError with a message that states what was given and what was expected.

Optional (defaulted) parameters are filled left to right with whatever is left after the required ones are satisfied. When the defaults are on the right, that is simply "the first ones". Ruby also allows a default in the middle, and then it fills the required parameters at both ends first. The documentation illustrates this with variables rather than words, and so does the example below: citation("Lin", 2024, "Quiet Streets") gives author and title their values first, and the one remaining argument goes to year, leaving edition at its default. RuboCop’s Style/OptionalArguments flags a default that is not at the end, which is the deliberate offence in this example; code you write rarely needs it.

# frozen_string_literal: true

module Shelf
  # Defaults on the right: the usual shape.
  def self.label(title, year = nil, edition = 1)
    [title, year, edition].inspect
  end

  # A default in the middle: allowed, but rarely a good idea.
  def self.citation(author, year = "n.d.", edition = 1, title)
    "#{author} (#{year}, ed. #{edition}): #{title}"
  end

  # A rest parameter collects the extras.
  def self.tags(first, *others) = [first, others]
end

puts Shelf.label("Quiet Streets")
puts Shelf.label("Quiet Streets", 2024)
puts Shelf.label("Quiet Streets", 2024, 3)

puts Shelf.citation("Lin", "Quiet Streets")
puts Shelf.citation("Lin", 2024, "Quiet Streets")
puts Shelf.citation("Lin", 2024, 3, "Quiet Streets")

p Shelf.tags(:fiction)
p Shelf.tags(:fiction, :city, :quiet)

[
  -> { Shelf.label },
  -> { Shelf.label("a", 1, 2, 3) },
  -> { Shelf.citation("Lin") }
].each do |call|
  call.call
rescue ArgumentError => e
  puts e.message
end
["Quiet Streets", nil, 1]
["Quiet Streets", 2024, 1]
["Quiet Streets", 2024, 3]
Lin (n.d., ed. 1): Quiet Streets
Lin (2024, ed. 1): Quiet Streets
Lin (2024, ed. 3): Quiet Streets
[:fiction, []]
[:fiction, [:city, :quiet]]
wrong number of arguments (given 0, expected 1..3)
wrong number of arguments (given 4, expected 1..3)
wrong number of arguments (given 1, expected 2..4)

An ArgumentError that escapes to the top of a program shows more than the message in Ruby 4.0: the ErrorHighlight extension (see the Ruby 4.0 NEWS) now prints a snippet of both the call and the method definition, so the wrong call and the signature it violates appear together.

# frozen_string_literal: true

module Shelf
  def self.lend(book, days) = "#{book} for #{days} days"
end

Shelf.lend("Quiet Streets")
ex15_arity_error.rb:4:in 'Shelf.lend': wrong number of arguments (given 1, expected 2) (ArgumentError)

    caller: ex15_arity_error.rb:7
    | ......
      ^^^^^
    callee: ex15_arity_error.rb:4
    | ......
      ^^^^^
	from ex15_arity_error.rb:7:in '<main>'

The ArgumentError messages are worth knowing by heart: wrong number of arguments (given 1, expected 2), (given 4, expected 1..3) for a range, (given 1, expected 2+) when there is a rest parameter, and a suffix such as ; required keyword: title when the method also has required keywords.

Unpacking an array with the splat

The splat * in a call turns a collection into separate positional arguments: show(*rows) is show("A", "B", "C"). It may appear anywhere in the list, more than once, and be mixed with ordinary arguments. The operator is not limited to arrays: Ruby calls to_a on the object and expects an Array back. An object without a to_a method is passed as one argument, which makes a splat a handy way to accept "one or many"; an object whose to_a returns something else raises a TypeError. The conversion rules are explained in Conversions and Coercion.

# frozen_string_literal: true

module Shelf
  def self.shelve(room, *rows) = "#{room}: #{rows.inspect}"

  def self.pair(left, right) = "#{left} | #{right}"

  # Responds to #to_a, so * can unpack it.
  class Reader
    def initialize(name) = @name = name

    def to_a = @name.split
  end

  # No #to_a: * passes the object itself.
  class Guest
    def initialize(name) = @name = name
  end

  # #to_a answers something that is not an Array.
  class Broken
    def to_a = "oops"
  end
end

rows = %w[A B C]
word = "single"
span = 1..3
nothing = nil

puts Shelf.shelve("north", *rows)
puts Shelf.shelve("north", "Z", *rows, "Y")
puts Shelf.shelve("north", *rows, *%w[D E])
puts Shelf.shelve("north", *nothing)
puts Shelf.shelve("north", *(rows if rows.empty?))
puts Shelf.shelve("north", *word)
puts Shelf.shelve("north", *span)
puts Shelf.shelve("north", *{ a: 1, b: 2 })

puts Shelf.pair(*Shelf::Reader.new("Ada Lovelace"))
puts Shelf.shelve("annex", *Shelf::Guest.new("Ada")).sub(/0x\h+/, "0x...")

begin
  Shelf.shelve("annex", *Shelf::Broken.new)
rescue TypeError => e
  puts e.message
end

# Array() is the explicit, never-raising cousin of the splat.
values = [nil, [1], 1..3, "a", { a: 1 }]
p(values.map { Array(it) })
pair = [1, 2]
p [*nothing, *word, *pair]

begin
  Shelf.pair(*rows)
rescue ArgumentError => e
  puts e.message
end
north: ["A", "B", "C"]
north: ["Z", "A", "B", "C", "Y"]
north: ["A", "B", "C", "D", "E"]
north: []
north: []
north: ["single"]
north: [1, 2, 3]
north: [[:a, 1], [:b, 2]]
Ada | Lovelace
annex: [#<Shelf::Guest:0x... @name="Ada">]
can't convert Shelf::Broken to Array (Shelf::Broken#to_a gives String)
[[], [1], [1, 2, 3], ["a"], [[:a, 1]]]
["single", 1, 2]
wrong number of arguments (given 3, expected 2)

Things the output shows:

  • a Hash is unpacked into its [key, value] pairs, and a Range into its elements;

  • *nil and *(x if cond) expand to nothing, which gives conditional arguments without a helper (call(*(extra if verbose)));

  • Kernel#Array is the explicit counterpart. Array(nil) is [], Array([1]) is [1] and Array("a") is ["a"]. Where the splat only tries to_a, Array() tries to_ary first and then to_a, and wraps anything else in an array; it is the clearest way to accept "one or many" values in a parameter;

  • an array that does not fit the method’s arity raises the usual ArgumentError: pair(*rows) with three rows gives wrong number of arguments (given 3, expected 2).

The Ruby 4.0 change to nil splats

Before Ruby 4.0, *nil called nil.to_a. In Ruby 4.0 the splat of nil is expanded to an empty list directly, just as **nil never called nil.to_hash (Ruby 4.0 NEWS, Feature #21047). The result is the same unless someone has redefined NilClass#to_a; the example below counts the conversions that happen by prepending a module to NilClass (a contrived but effective probe). Neither splat touches the conversion methods any more: the counter stays empty.

# frozen_string_literal: true

# Count every call to NilClass#to_a (and to_hash).
module CountNilConversions
  def self.counts = @counts ||= Hash.new(0)

  def to_a
    CountNilConversions.counts[:to_a] += 1
    super
  end

  def to_hash
    CountNilConversions.counts[:to_hash] += 1
    {}
  end
end
NilClass.prepend(CountNilConversions)

def gather(*list, **opts) = [list, opts]

nothing = nil
p gather(*nothing)
p gather(**nothing)
p gather(*nothing, **nothing)
p CountNilConversions.counts
[[], {}]
[[], {}]
[[], {}]
{}

Keyword splatting of nil in a call (**nil) became legal in Ruby 3.4, with the same meaning as **{} (pass no keywords and call no conversion method). Both are safe for optional extras; neither is a replacement for validating required values.

Keyword arguments

A keyword argument is written name: value after the positional arguments. Keywords are matched to parameters by name, so their order at the call site does not matter, and any keyword with a default can be left out. A missing required keyword, an unknown keyword or a positional value where a keyword was meant each produce an ArgumentError that names the problem: missing keyword: :year, missing keywords: :title, :year, unknown keyword: :lmit, unknown keywords: :lmit, :srt, and, when one passes positionals to a keyword-only method, wrong number of arguments (given 2, expected 0; required keywords: title, year).

# frozen_string_literal: true

module Shelf
  Book = Data.define(:title, :year)

  def self.search(term, limit: 10, sort: :title, year: nil)
    "search #{term.inspect} limit=#{limit} sort=#{sort} year=#{year.inspect}"
  end

  def self.register(title:, year:) = Book.new(title:, year:)

  def self.attempt
    yield
  rescue ArgumentError => e
    puts e.message
  end
end

puts Shelf.search("quiet")
puts Shelf.search("quiet", limit: 3)
puts Shelf.search("quiet", sort: :year, limit: 3)

Shelf.attempt { Shelf.search(limit: 5) }

Shelf.attempt { Shelf.search("quiet", lmit: 3) }
Shelf.attempt { Shelf.search("quiet", lmit: 3, srt: :year) }
Shelf.attempt { Shelf.register(title: "Quiet Streets") }
Shelf.attempt { Shelf.register }
Shelf.attempt { Shelf.register("Quiet Streets", 2024) }

# Value omission (Ruby 3.1): a bare `key:` reads the local variable or
# method with the same name.
title = "Quiet Streets"
year = 2024
p Shelf.register(title:, year:)
p({ title:, year: })

# Keyword arguments may be passed in any order.
p Shelf.register(year: 1999, title: "Old Roofs")
search "quiet" limit=10 sort=title year=nil
search "quiet" limit=3 sort=title year=nil
search "quiet" limit=3 sort=year year=nil
wrong number of arguments (given 0, expected 1)
unknown keyword: :lmit
unknown keywords: :lmit, :srt
missing keyword: :year
missing keywords: :title, :year
wrong number of arguments (given 2, expected 0; required keywords: title, year)
#<data Shelf::Book title="Quiet Streets", year=2024>
{title: "Quiet Streets", year: 2024}
#<data Shelf::Book title="Old Roofs", year=1999>

Value omission

Since Ruby 3.1, f(x:, y:) is a shorthand for f(x: x, y: y): the value is taken from the local variable or method named like the key, and the same works in hash literals ({ title:, year: }). It removes the repetition that dominates calls with keyword parameters, at the price that the name must match.

The one trap concerns calls without parentheses. A value-omitted keyword at the end of a line pulls in the following line:

# frozen_string_literal: true

title = "Quiet Streets"
year = 2024

def register(title:, year:) = [title, year]

# With parentheses, the omitted value ends at the closing parenthesis.
p(register(title:, year:))

# Without them, the next line becomes the value of `year:`.
p register title:, year:
42
["Quiet Streets", 2024]
["Quiet Streets", 42]

The last two lines parse as p register(title:, year: 42), not as two statements, because the call has no closing parenthesis to stop at; the documentation warns about exactly this. Use parentheses whenever a call ends in an omitted value.

Unpacking a hash with the double splat

** in a call turns a hash into keyword arguments, the way * turns an array into positionals. The expression may appear several times and be mixed with literal keywords. When the same key appears more than once, the last one wins, which is how defaults and overrides are layered: **defaults, **overrides. The operator works on any object that has to_hash, and, unlike the splat, it raises a TypeError for an object that does not respond to to_hash (only nil is exempt, since Ruby 3.4). An empty **{} passes nothing at all.

On the receiving end, **opts gathers any keywords that were not named by other parameters, including keys that are not symbols, and **nil in a definition declares that the method accepts no keywords. Both appear below.

# frozen_string_literal: true

module Shelf
  DEFAULTS = { limit: 10, sort: :title }.freeze

  def self.search(term, limit:, sort:, year: nil)
    "#{term}: limit=#{limit} sort=#{sort} year=#{year.inspect}"
  end

  # Gathers whatever keywords were not named, including non-Symbol keys.
  def self.tagged(title, **tags) = [title, tags]

  # Rejects every keyword explicitly.
  def self.strict(title, **nil) = title

  # An object that converts itself to keywords.
  class Query
    def initialize(year)
      @year = year
    end

    def to_hash = { limit: 5, sort: :year, year: @year }
  end
end

filters = { limit: 3, year: 2024 }

puts Shelf.search("quiet", **Shelf::DEFAULTS)
puts Shelf.search("quiet", **Shelf::DEFAULTS, **filters)
puts Shelf.search("quiet", **filters, **Shelf::DEFAULTS)
puts Shelf.search("quiet", sort: :year, **filters)

extra = nil
puts Shelf.search("quiet", **Shelf::DEFAULTS, **extra)

p Shelf.tagged("Quiet Streets", genre: "city", "isbn" => "9780306406157")
p Shelf.tagged("Quiet Streets", **{})
p Shelf.strict("Quiet Streets", **nil)

begin
  Shelf.strict("Quiet Streets", genre: "city")
rescue ArgumentError => e
  puts e.message
end

puts Shelf.search("quiet", **Shelf::Query.new(2024))

begin
  Shelf.search("quiet", **"limit")
rescue TypeError => e
  puts e.message
end
quiet: limit=10 sort=title year=nil
quiet: limit=3 sort=title year=2024
quiet: limit=10 sort=title year=2024
quiet: limit=3 sort=year year=2024
quiet: limit=10 sort=title year=nil
["Quiet Streets", {genre: "city", "isbn" => "9780306406157"}]
["Quiet Streets", {}]
"Quiet Streets"
no keywords accepted
quiet: limit=5 sort=year year=2024
no implicit conversion of String into Hash

Two points to notice in the output. First, Shelf.search("quiet", **filters, **Shelf::DEFAULTS) returns limit=10, because the defaults came last and overwrote the filter. Second, strict("Quiet Streets", genre: "city") is rejected with no keywords accepted: a **nil method is the way to forbid keywords, so that a trailing hash is never silently reinterpreted.

Hash arguments and the separation of keywords from positionals

Until Ruby 2.6 a trailing Hash argument and keyword arguments were interchangeable: a hash passed as the last argument of a method with keyword parameters was converted to keywords, and keywords passed to a method without keyword parameters were converted to a hash. This led to confusing corner cases (an optional hash followed by keywords, a hash with string keys, an empty hash). Ruby 2.7 warned about the cases that would change, and Ruby 3.0 separated the two: keywords are keywords and a Hash is a positional value. What remains is explicit: **hash turns a Hash into keywords, and a method without keyword parameters collects braceless keywords into a trailing Hash.

Call Ruby 3.0 and later Ruby 2.6 and earlier

f(key: 1) to def f(opts) or def f(opts = {})

the braceless keywords are collected as a Hash and passed positionally

the same

f({ key: 1 }) to def f(key: 0)

ArgumentError: a Hash is one positional argument

converted to keywords

f(**hash) to def f(key: 0)

keywords

keywords

f(hash) to def f(key: 0)

ArgumentError

converted to keywords (warned in 2.7)

The only call that is accepted both ways is the first: a method without keyword parameters still receives braceless keywords as a trailing hash, so APIs that take an options hash keep working. The Shelf methods below show all three receiving styles (keywords only, trailing hash only, both) and what each call site gets.

# frozen_string_literal: true

module Shelf
  # Accepts keywords.
  def self.search(term, limit: 10) = "search #{term} limit=#{limit}"

  # Accepts a trailing options hash and no keywords.
  def self.legacy(term, options = {}) = "legacy #{term} options=#{options}"

  # Accepts both.
  def self.both(term, options = {}, **keywords)
    "both #{term} options=#{options} keywords=#{keywords}"
  end

  def self.attempt
    puts yield
  rescue ArgumentError => e
    puts "ArgumentError: #{e.message}"
  end
end

params = { limit: 3 }

# Braceless keywords reach a method without keyword parameters as a Hash.
Shelf.attempt { Shelf.legacy("quiet", limit: 3) }
Shelf.attempt { Shelf.legacy("quiet", { limit: 3 }) }
Shelf.attempt { Shelf.legacy("quiet", params) }

# A Hash is not turned into keywords any more; ** says what you mean.
Shelf.attempt { Shelf.search("quiet", params) }
Shelf.attempt { Shelf.search("quiet", **params) }
Shelf.attempt { Shelf.search("quiet", **{}) }

# With both kinds of parameters the call site decides.
Shelf.attempt { Shelf.both("quiet", params) }
Shelf.attempt { Shelf.both("quiet", **params) }
Shelf.attempt { Shelf.both("quiet", params, **params) }
legacy quiet options={limit: 3}
legacy quiet options={limit: 3}
legacy quiet options={limit: 3}
ArgumentError: wrong number of arguments (given 2, expected 1)
search quiet limit=3
search quiet limit=10
both quiet options={limit: 3} keywords={}
both quiet options={} keywords={limit: 3}
both quiet options={limit: 3} keywords={limit: 3}

The rest of the 3.0 break was in blocks and wrappers:

  • a proc with a single rest parameter and keywords no longer autosplats (3.0), and a proc with one positional parameter and keywords no longer autosplats (3.2);

  • keyword arguments (and block arguments) are no longer allowed in an index assignment such as a[0, kw: 1] = 2 (3.4), because the order of evaluation was never well defined;

  • a method that delegates with *args lost keywords unless it was marked with ruby2_keywords (see below). Ruby 3.2 also made the marking mandatory for every such method, with no exceptions.

ruby2_keywords

Before (...) existed, the idiom for forwarding was def f(*args, &block); g(*args, &block); end, and in Ruby 2.x the last keyword argument travelled inside args as a hash that was converted back to keywords at the next call. Ruby 3 needs a marker for that conversion: ruby2_keywords flags the method so that a final hash built from keywords carries a hidden flag (Hash.ruby2_keywords_hash?), and a later *args splat turns the flagged hash back into keywords. Module#ruby2_keywords marks methods, Proc#ruby2_keywords marks procs, and both exist for code that still has to run on Ruby 2.6 and 3.x. New code should use (...) or an explicit *args, **keywords.

# frozen_string_literal: true

module Shelf
  class Store
    def add(title, copies: 1) = "add #{title.inspect} copies=#{copies}"
  end

  # The pre-2.7 delegation idiom, unchanged: the keywords are lost in Ruby 3.
  class Naive
    def initialize(store)
      @store = store
      @log = []
    end

    def add(*args)
      @log << args
      @store.add(*args)
    end
  end

  # The same idiom, flagged so that the final Hash is passed on as keywords.
  class Flagged
    def initialize(store)
      @store = store
      @log = []
    end

    def add(*args)
      @log << args
      @store.add(*args)
    end
    ruby2_keywords :add
  end

  # The explicit form needs no flag.
  class Explicit
    def initialize(store)
      @store = store
      @log = []
    end

    def add(*args, **keywords)
      @log << [args, keywords]
      @store.add(*args, **keywords)
    end
  end

  # Shows what ruby2_keywords changes: the last Hash carries a flag.
  class Probe
    def plain?(*args) = Hash.ruby2_keywords_hash?(args.last)

    def flagged?(*args) = Hash.ruby2_keywords_hash?(args.last)
    ruby2_keywords :flagged?
  end
end

store = Shelf::Store.new

begin
  puts Shelf::Naive.new(store).add("Quiet Streets", copies: 2)
rescue ArgumentError => e
  puts "ArgumentError: #{e.message}"
end
puts Shelf::Flagged.new(store).add("Quiet Streets", copies: 2)
puts Shelf::Explicit.new(store).add("Quiet Streets", copies: 2)

probe = Shelf::Probe.new
p probe.plain?(1, copies: 2)
p probe.flagged?(1, copies: 2)
p probe.flagged?(1, { copies: 2 })
ArgumentError: wrong number of arguments (given 2, expected 1)
add "Quiet Streets" copies=2
add "Quiet Streets" copies=2
false
true
false

The third Probe call shows the flag is only set when the caller used keywords: the braced hash flagged?(1, { copies: 2 }) is a plain hash and is not turned into keywords. The development version of the Ruby documentation (the NEWS for the next release, 4.1) lists Module#ruby2_keywords and Proc#ruby2_keywords as deprecated and Hash.ruby2_keywords_hash? as deprecated for later removal; these are still current in 4.0, but new libraries should not start using them.

Taming long argument lists

A call with many arguments is hard to read and easy to get wrong, even with keywords. Two remedies are idiomatic. The first is to turn the loose arguments into keywords with sensible defaults, as search did above. The second, when the same group of values travels through several methods, is a parameter object: a Data value (see Struct, Data and Value Objects) that names the group, validates it once in initialize, and is passed as one argument. A Hash of options becomes a Query with Query.new(**options), and the keyword checks of the Ruby runtime (unknown or missing keywords) come for free.

# frozen_string_literal: true

module Shelf
  # Six loose arguments become one named value.
  Query = Data.define(:term, :limit, :sort, :year) do
    def initialize(term:, limit: 10, sort: :title, year: nil)
      raise ArgumentError, "limit must be positive: #{limit}" unless limit.positive?

      super
    end
  end

  def self.search(query)
    "#{query.term}: limit=#{query.limit} sort=#{query.sort} year=#{query.year.inspect}"
  end

  # The same query from a Hash of options, e.g. parsed from a request.
  def self.search_with(options) = search(Query.new(**options))
end

puts Shelf.search(Shelf::Query.new(term: "quiet"))
puts Shelf.search(Shelf::Query.new(term: "quiet", year: 2024, limit: 3))
puts Shelf.search_with({ term: "loud", sort: :year })

query = Shelf::Query.new(term: "quiet")
p query.with(limit: 50).limit
p query.to_h

begin
  Shelf::Query.new(term: "quiet", limit: 0)
rescue ArgumentError => e
  puts e.message
end

begin
  Shelf.search_with({ term: "quiet", colour: "red" })
rescue ArgumentError => e
  puts e.message
end
quiet: limit=10 sort=title year=nil
quiet: limit=3 sort=title year=2024
loud: limit=10 sort=year year=nil
50
{term: "quiet", limit: 10, sort: :title, year: nil}
limit must be positive: 0
unknown keyword: :colour

Other habits that keep call sites readable: keep methods short enough that their parameter list stays short; give boolean parameters a keyword (lend(book, renew: true) rather than lend(book, true)); return one type from a method; and prefer a guard clause that raises ArgumentError for an invalid value over silent coercion. The definition side of these points is on Methods and Parameters and the style rules on Idioms, Style and Clean Code.

Argument forwarding

Delegation is everywhere: decorators, proxies, wrappers around a store, service objects that pass their arguments to new. The forwarding syntax lets a method pass whatever it received, including keywords and the block, without naming or inspecting any of it.

Forwarding everything with an ellipsis

Since Ruby 2.7 a method can declare ... as its only parameter and pass the same ... as an argument: all positional arguments, all keywords and the block are forwarded as they came. The syntax is allowed only in a call made from a method that declares ...; anywhere else it is a SyntaxError. Since Ruby 3.0 there can also be leading arguments before the ..., both in the definition (positional parameters without defaults) and in the call. That makes it possible to inspect or consume the first argument and still forward the rest.

Four common uses are in the example:

  • a decorator that logs and then hands everything on (LoggedStore#add), with a leading argument in the definition and the call (count(title, ...));

  • the method_missing and respond_to_missing? pair, forwarding any other message, which is the Ruby 3.0 NEWS' own example of leading arguments;

  • a service object whose class method forwards to new and then runs the instance (def self.call(...) = new(...).call), a style used in the plain-Ruby service layer on Application Architecture in Plain Ruby;

  • a subclass whose constructor is declared with ... can use a bare super to hand the whole list up (ShortLend).

# frozen_string_literal: true

module Shelf
  Book = Data.define(:title)

  class Store
    def initialize = @books = []

    def add(book, copies: 1, &note)
      @books.concat([book] * copies)
      note&.call(book)
      @books.size
    end

    def count(title) = @books.count { it.title == title }

    def titles = @books.map(&:title).uniq
  end

  # A decorator: every call is logged, then handed on untouched.
  class LoggedStore
    def initialize(store, out: $stdout)
      @store = store
      @out = out
    end

    # (...) forwards positional arguments, keywords and the block.
    def add(...)
      @out.puts "add called"
      @store.add(...)
    end

    # A leading argument may precede (...), in both the definition
    # and the call (Ruby 3.0).
    def count(title, ...)
      @out.puts "count #{title.inspect}"
      @store.count(title, ...)
    end

    # Forward anything else, keeping respond_to? truthful.
    def method_missing(name, ...)
      return super unless @store.respond_to?(name)

      @out.puts "forwarding #{name}"
      @store.public_send(name, ...)
    end

    def respond_to_missing?(name, include_private = false)
      @store.respond_to?(name, include_private) || super
    end
  end

  # A service object whose class method forwards to the instance.
  class Lend
    def self.call(...) = new(...).call

    def initialize(book, days: 14, reader: "anonymous")
      @book = book
      @days = days
      @reader = reader
    end

    def call = "#{@reader} borrows #{@book.title.inspect} for #{@days} days"
  end

  # super(...) hands the whole argument list to the superclass.
  class ShortLend < Lend
    def initialize(book, ...)
      super
      @days = [@days, 7].min
    end
  end
end

store = Shelf::LoggedStore.new(Shelf::Store.new)
book = Shelf::Book.new("Quiet Streets")

p store.add(book, copies: 2) { puts "noted #{it.title}" }
p store.count("Quiet Streets")
p store.titles
p store.respond_to?(:titles), store.respond_to?(:delete)

puts Shelf::Lend.call(book)
puts Shelf::Lend.call(book, days: 21, reader: "Lin")
puts Shelf::ShortLend.call(book, days: 21)

begin
  store.delete(book)
rescue NoMethodError => e
  puts e.message
end

p Shelf::LoggedStore.instance_method(:add).parameters
p Shelf::LoggedStore.instance_method(:count).parameters
p Shelf::Lend.method(:call).parameters
add called
noted Quiet Streets
2
count "Quiet Streets"
2
forwarding titles
["Quiet Streets"]
true
false
anonymous borrows "Quiet Streets" for 14 days
Lin borrows "Quiet Streets" for 21 days
anonymous borrows "Quiet Streets" for 7 days
undefined method 'delete' for an instance of Shelf::LoggedStore
[[:rest, :*], [:keyrest, :**], [:block, :&]]
[[:req, :title], [:rest, :*], [:keyrest, :**], [:block, :&]]
[[:rest, :*], [:keyrest, :**], [:block, :&]]

The Method#parameters listing at the end shows what ... really is: an anonymous [:rest, :*], an anonymous [:keyrest, :**] and an anonymous [:block, :&], plus the leading [:req, :title].

Restrictions and pitfalls:

  • ... cannot be combined with other rest, keyword or block parameters in the same definition. A method that must inspect the keywords uses *args, **keywords, &block instead.

  • A call that forwards ... must not drop the parentheses. puts ... at the end of a line is parsed as the endless range puts()... and prints an empty line, and Ruby warns:

$ ruby -w -e 'def log(...)
  puts ...
end
log("test")'
-e:2: warning: ... at EOL, should be parenthesized?

$ ruby -e 'def regular(arg, **kwarg) = concrete(...)'
-e: -e:1: syntax error found (SyntaxError)
> 1 | ... ...)
    |     ^~~ unexpected ... when the parent method is not forwarding

Anonymous rest, keyword rest and block parameters

Forwarding can also be done piece by piece, naming nothing. A method may declare a bare *, ** or &, and then pass the same bare symbol on:

Since Definition Call

Ruby 2.7

def f(...)

g(...)

Ruby 3.1

def each(&)

@books.each(&)

Ruby 3.2

def find(*, **)

match?(it, *, **)

The anonymous forms are for the case ... cannot express: forwarding only part of the arguments, or forwarding the same rest array to two callees, or giving each piece to a different call.

# frozen_string_literal: true

module Shelf
  Book = Data.define(:title, :year)

  class Catalog
    include Enumerable

    def initialize(books) = @books = books

    # Anonymous block forwarding (Ruby 3.1).
    def each(&)
      return enum_for(:each) unless block_given?

      @books.each(&)
    end

    # Anonymous rest and keyword rest forwarding (Ruby 3.2).
    def find_all_by(*, **)
      @books.select { match?(it, *, **) }
    end

    # All three at once, handing the pieces to different callees.
    def report(*titles, **options, &)
      select { titles.include?(it.title) }.map(&).join(options.fetch(:sep, ", "))
    end

    private

    def match?(book, *titles, year: nil)
      (titles.empty? || titles.include?(book.title)) &&
        (year.nil? || book.year == year)
    end
  end
end

catalog = Shelf::Catalog.new(
  [
    Shelf::Book.new("Quiet Streets", 2024),
    Shelf::Book.new("Loud Roofs", 2021),
    Shelf::Book.new("Quiet Rooms", 2024)
  ]
)

p catalog.map(&:title)
p catalog.find_all_by(year: 2024).map(&:title)
p catalog.find_all_by("Loud Roofs", year: 2021).map(&:title)
puts catalog.report("Quiet Streets", "Loud Roofs", sep: " / ") { it.title.upcase }

p Shelf::Catalog.instance_method(:each).parameters
p Shelf::Catalog.instance_method(:find_all_by).parameters
["Quiet Streets", "Loud Roofs", "Quiet Rooms"]
["Quiet Streets", "Quiet Rooms"]
["Loud Roofs"]
QUIET STREETS / LOUD ROOFS
[[:block, :&]]
[[:rest, :*], [:keyrest, :**]]

Rules:

  • The anonymous block parameter & was introduced in Ruby 3.1 (Feature #11256); the anonymous rest and keyword rest in Ruby 3.2 (Feature #18351). Before 3.2 a method could declare * or ** but not pass them on.

  • Inside a block, *, ** and & still refer to the enclosing method’s anonymous parameter, but not if the block declares an anonymous parameter of its own (shown after this list). Blocks written with it, with _1 or with a named parameter are fine, as in find_all_by.

  • RuboCop’s Style/ArgumentsForwarding suggests the anonymous forms when a rest, keyword rest or block parameter is named but only forwarded, and Naming/BlockForwarding prefers & to &block when the block is only passed on. Both cops come with the default configuration, which is why the examples use the anonymous syntax.

  • A forwarded * is not shared with **: a method with * and ** that is called with keywords gathers them in **, never in *, and the call f(1, 2, "3" => 4, five: 6) has the positional list [1, 2] and the keywords {"3" => 4, five: 6}.

Ruby 4.0 rejects a forwarded anonymous parameter inside a block that declares an anonymous parameter of its own, at parse time:

$ ruby -e 'def m(*) = [1].each { |*| p(*) }'
-e: -e:1: syntax error found (SyntaxError)
> 1 | def m(*) = [1].each { |*| p(*) }
    |                             ^ unexpected `*`; anonymous rest parameter is also used within block

Adapters, decorators and service objects are the usual homes of forwarding; for the design patterns themselves see SOLID and Design Patterns.

Bare super and explicit super

The forwarding rules also apply to super: it is the call to the next method in the lookup chain (Method Lookup and super). Its three forms differ in exactly what they forward:

Form Passes

super

The current values of all the method’s parameters, including keywords and the block. Reassigning a parameter before the call changes what goes up. With a ... definition, everything.

super()

No arguments at all. The method’s block is still passed implicitly.

super(a, key: b)

Exactly the listed arguments. The block is passed implicitly as well; super(&nil) suppresses it.

# frozen_string_literal: true

module Shelf
  class Lend
    attr_reader :book, :days, :reader

    def initialize(book, days: 14, reader: "anonymous", &note)
      @book = book
      @days = days
      @reader = reader
      note&.call(self)
    end
  end

  # Bare super passes the *current* values of the parameters, keywords
  # and block included.
  class Renewal < Lend
    def initialize(book, days: 14, reader: "anonymous", &)
      days += 7
      super
    end
  end

  # super(...) is explicit about what goes up.
  class Express < Lend
    def initialize(book, reader: "express")
      super(book, days: 1, reader:)
    end
  end

  # super() passes nothing at all.
  class Broken < Lend
    def initialize(book, days: 14)
      @requested = [book, days]
      super()
    end
  end
end

renewal = Shelf::Renewal.new("Quiet Streets", days: 7, reader: "Lin") { puts "noted #{it.reader}" }
p renewal.days, renewal.reader

express = Shelf::Express.new("Quiet Streets")
p [express.days, express.reader]

begin
  Shelf::Broken.new("Quiet Streets")
rescue ArgumentError => e
  puts e.message
end
noted Lin
14
"Lin"
[1, "express"]
wrong number of arguments (given 0, expected 1)

In Renewal the call Renewal.new("Quiet Streets", days: 7, ...) ends with 14 days, not 7, because bare super passed the already incremented local. Bare super is not only for positional arguments: it forwards keywords too (the reader: value arrives in Lend), and the block ({ puts ... }) is passed along as well. A bare super is therefore the compact way to extend a constructor, and super() the way to call a superclass method with no arguments at all.

Block arguments

The block is the last argument and is written outside the parentheses, with do ... end or braces, or as an expression after an ampersand in the parentheses. The block receives the arguments the method yields, and its parameters follow the same rules as a method’s (with the difference that a block, unlike a lambda, is lenient about arity). What a block is is on Blocks, Procs and Lambdas; this section is about passing one.

Which call gets the block

do ... end binds with lower precedence than braces, so in a call with a nested call the two forms attach to different methods. In outer inner do ... end, the block goes to outer; in outer inner { ... }, and in anything parenthesized, it goes to inner. RuboCop’s Lint/AmbiguousBlockAssociation warns about the braced form without parentheses, Style/BlockDelimiters pushes multi-line blocks to do ... end and single-line ones to braces, and Style/SingleLineDoEndBlock is the reason the parenthesized case below is spread over three lines.

# frozen_string_literal: true

# Reports whether it received a block, and calls it if so.
def outer(arg, &block)
  puts "outer: block=#{block ? block.call : 'none'}, arg=#{arg.inspect}"
end

def inner(&block)
  puts "inner: block=#{block ? block.call : 'none'}"
  :inner_result
end

# Braces bind to the nearest call, inner.
outer(inner { :braces })

# Inside parentheses, do...end binds to inner as well.
outer(inner do
  :do_end
end)

# Without parentheses, do...end binds to the outermost call, outer.
outer inner do
  :do_end
end
inner: block=braces
outer: block=none, arg=:inner_result
inner: block=do_end
outer: block=none, arg=:inner_result
inner: block=none
outer: block=do_end, arg=:inner_result

A block passed to a method that never uses it is silently dropped by the language. In verbose mode (-w) Ruby 4.0 reports the likely mistake, as shown below. Checking block_given? alone does not count as using the block: the method must yield or take it as &block.

$ ruby -w -e 'def label(book) = book
label("Quiet Streets") { }'
-e:2: warning: the block passed to 'Object#label' defined at -e:1 may be ignored

Converting an object to a block

An ampersand before the last argument converts it into the block: list.map(&callable). If the object is a Proc, it is used as is (a lambda stays a lambda, with strict arity); otherwise Ruby calls to_proc on it. nil means "no block", which is how a method can forward an optional block unconditionally. Anything else is a TypeError.

  • &:name is Symbol#to_proc: the symbol becomes a block that calls that method on its first argument. The proc is a lambda since Ruby 3.0 (:upcase.to_proc.lambda? is true), has arity -2 and takes the receiver as the first argument and any more as arguments to the method;

  • &method(:name) turns a Method object into a block (Method#to_proc), which is a lambda too;

  • any object that defines to_proc works, so a class can offer a reusable filter or formatter as a block.

# frozen_string_literal: true

module Shelf
  Book = Data.define(:title, :year)

  def self.shout(title) = "#{title.upcase}!"

  # Any object with #to_proc can follow &.
  class YearFilter
    def initialize(year) = @year = year

    def to_proc = ->(book) { book.year == @year }
  end

  def self.each_title(books)
    return enum_for(__method__, books) unless block_given?

    books.each { yield it.title }
  end
end

books = [
  Shelf::Book.new("Quiet Streets", 2024),
  Shelf::Book.new("Loud Roofs", 2021)
]

# &:symbol calls that method on each element.
p books.map(&:title)
p books.sum(&:year)

# &method(:name) turns a method into a block.
p books.map(&:title).map(&Shelf.method(:shout))
p %w[7 8].map(&method(:Integer))

# Any object that answers to_proc.
p books.select(&Shelf::YearFilter.new(2024)).map(&:title)

# A plain block unpacks an Array argument into its parameters ...
pair = ->(left, right) { "#{left}-#{right}" }
p([[1, 2]].map { |left, right| pair.call(left, right) })

# ... hash iteration hands a two-parameter lambda the key and value separately ...
p({ a: 1 }.map(&pair))

# ... but a lambda keeps its strict arity, and gets one Array here.
begin
  [[1, 2]].map(&pair)
rescue ArgumentError => e
  puts e.message
end

# Anything that is not a Proc, Method, Symbol or to_proc object is a TypeError.
begin
  books.map(&5)
rescue TypeError => e
  puts e.message
end
Shelf.each_title(books) { puts it }
p Shelf.each_title(books).to_a
# &nil passes no block at all.
p Shelf.each_title(books, &nil).class

# A block-local variable (after the semicolon) never leaks out.
place = "world"
[1].each do |n; place|
  place = "block #{n}"
  puts place
end
puts place
["Quiet Streets", "Loud Roofs"]
4045
["QUIET STREETS!", "LOUD ROOFS!"]
[7, 8]
["Quiet Streets"]
["1-2"]
["a-1"]
wrong number of arguments (given 1, expected 2)
no implicit conversion of Integer into Proc
Quiet Streets
Loud Roofs
["Quiet Streets", "Loud Roofs"]
Enumerator
block 1
world

The point to remember about Symbol#to_proc is that the symbol names a method of the first argument and any further block arguments are passed to that method: inject(&:+) works because the first value becomes the receiver and the second the argument.

Block-local variables, declared after a semicolon in the block parameters (|n; place|), shadow an outer variable of the same name for the duration of the block. They protect the caller’s variables from the block; the last lines of the example show that place is still "world" afterwards.

Calling a method by name at run time

When the method name is data, use public_send (or send). Both pass the remaining arguments, keywords and block through unchanged. send ignores visibility and can call a private method; public_send respects it and raises the usual NoMethodError. The documentation also provides __send__, a copy that survives a class defining its own send. Use public_send unless the code is deliberately reaching into internals, as test code sometimes does.

# frozen_string_literal: true

module Shelf
  Book = Data.define(:title, :year) do
    def label(prefix = "", suffix: "") = "#{prefix}#{title}#{suffix}"

    private

    def secret = "internal: #{title}"
  end
end

book = Shelf::Book.new("Quiet Streets", 2024)
field = :label
hidden = :secret

# The method name is data; arguments, keywords and a block pass through.
puts book.public_send(field, "> ", suffix: " <")
puts book.send(field)

# public_send honours visibility, send does not.
puts book.send(hidden)
begin
  book.public_send(hidden)
rescue NoMethodError => e
  puts e.message
end

# A Method object is a callable reference with the receiver bound.
bound = book.method(:label)
puts bound.call("* ")
puts bound.unbind.bind_call(Shelf::Book.new("Loud Roofs", 2021))
> Quiet Streets <
Quiet Streets
internal: Quiet Streets
private method 'secret' called for an instance of Shelf::Book
* Quiet Streets
Loud Roofs

Never pass an unchecked user-supplied string to send: it can reach any method, including private and destructive ones (see Security). Dynamic dispatch, method_missing, define_method and the rest of the reflection toolkit are in Dynamic Dispatch and Method Definition and Reflection, Introspection and Tracing.

How a call finds its method

When a message is sent, Ruby looks for a method with that name in the receiver’s singleton class, then its prepended modules, its class, its included modules, and then the same for each superclass. If nothing is found, the search is repeated for method_missing (whose default, BasicObject#method_missing, raises NoMethodError or NameError). Refinements, when active, change the order. The full algorithm, with a diagram, is on Method Lookup and super; the call syntax in this page does not affect it.

Where the books are outdated

The two books this reference draws on (see the bibliography on the landing page) are good on the idea of each topic on this page and predate or miss several of the modern forms:

Statement in the books Current behaviour

Error messages that name the class of a nil receiver, as in ... for nil:NilClass, with a backtick before the method name.

Ruby 3.3 shows the receiver (for nil, for an instance of Shelf::Card), and Ruby 3.4 uses straight quotes. In 4.0: undefined method 'upcase' for nil.

Hashes printed as {:name=>"Ico"}.

Ruby 3.4 prints {name: "Ico"} and {"a" => 1}, with spaces around =>.

Double splat and keyword arguments covered, but without value omission, without anonymous *, **, &, and without **nil.

Value omission (3.1), anonymous block (3.1), anonymous rest and keyword rest (3.2), **nil in calls (3.4), *nil without to_a (4.0).

Symbol#to_proc attributed to Ruby 1.9.

It exists since Ruby 1.8.7, and since Ruby 3.0 the proc it returns is a lambda.

Blocks written with _1 only.

Ruby 3.4 adds it, which is also safe next to a forwarded anonymous parameter (see above).

Common errors and warnings

Message Usual cause

wrong number of arguments (given 1, expected 2)

Wrong positional count. In 4.0 the report also shows the call and the definition. A (given 1, expected 0; required keyword: title) suffix means a required keyword was passed positionally.

missing keyword: :year / unknown keyword: :yr

A required keyword is absent, or a keyword name is misspelled (or the receiving method has no **opts).

no keywords accepted

The method was defined with **nil and was called with keywords.

no implicit conversion of String into Hash

A **value whose value is not a hash.

can't convert Shelf::Broken to Array (Shelf::Broken#to_a gives String)

A splat of an object whose to_a returns something other than an Array.

no implicit conversion of Integer into Proc

&value where the value is not a Proc, a Method, a Symbol or a to_proc object.

undefined method 'x' for nil

A nil receiver; consider &. only if nil is expected.

private method 'x' called for an instance of Y

A private method called with an explicit receiver other than self.

unexpected ... when the parent method is not forwarding

... used in a method that does not declare ....

anonymous rest parameter is also used within block

*, ** or & forwarded from inside a block that has its own anonymous parameter.

warning: ambiguous ... has been interpreted as an argument prefix

A splat, ** or & as the first argument of a call without parentheses.

warning: ... at EOL, should be parenthesized?

A puts ... forwarding call without parentheses.

warning: the block passed to 'X#m' ... may be ignored

A block was given to a method that never uses it.

For the error-handling side (rescuing ArgumentError at an API boundary, raising it from guard clauses) see Exceptions and Error Handling. For frequently seen pitfalls overall see Common Mistakes and Gotchas.

Version notes

Release Change

Ruby 2.7

(...) argument forwarding; calling a private method with a literal self receiver; warnings for the keyword changes of 3.0.

Ruby 3.0

Keyword arguments separated from positionals (Feature #14183); leading arguments before (...) (Feature #16378); Symbol#to_proc returns a lambda (Feature #16260).

Ruby 3.1

Value omission f(x:, y:) (Feature #14579); anonymous block parameter & (Feature #11256).

Ruby 3.2

Anonymous * and ** can be passed on (Feature #18351); every *args delegator needs ruby2_keywords; a proc with one positional parameter and keywords no longer autosplats.

Ruby 3.3

NoMethodError messages show the receiver (for nil, for an instance of X).

Ruby 3.4

**nil allowed in calls (Bug #20064); keyword and block arguments forbidden in index assignment; the it block parameter; straight quotes in error messages.

Ruby 4.0

*nil no longer calls nil.to_a (Feature #21047); a leading && or || continues the previous line; ArgumentError reports show the call and the definition.

Ruby 4.1 (upcoming)

Module#ruby2_keywords, Proc#ruby2_keywords and Hash.ruby2_keywords_hash? are deprecated in the development NEWS. Not part of Ruby 4.0.