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:
-
rendercallsheadingandbylinewith the implicit receiver. They areprivate, which only forbids calling them with an explicit receiver;card.headingfrom outside raisesNoMethodErrorwith the messageprivate 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’sinspect). -
self.renders = renders + 1writes through a private writer with an explicitselfreceiver. For a setter this is required (a barerenders = 1assigns a local variable), and Ruby 2.7 relaxed the rule so that any private method may be called asself.name. The readerrendersis called with the implicit receiver. See Visibility and Encapsulation for the full rules. -
the last case calls
upcaseonnil. Anilreceiver is the commonest source ofNoMethodError; the message names the receiver asnil, not asNilClass(theundefined method 'x' for nil:NilClasswording 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 |
|---|---|
|
The usual call, with an explicit receiver. |
|
Implicit receiver |
|
Explicit |
|
The |
|
|
|
Syntax sugar for |
|
Calls |
|
Safe navigation: calls the method only if the receiver is not |
Source: Ruby documentation: Calling Methods and Proc#call
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) * 3passes the whole expression(1 + 2) * 3as the argument;puts(1 + 2) * 3callsputswith3and then multiplies itsnilresult by 3. -
Nested command calls need parentheses.
b 1, a 2, 3is aSyntaxErrorbecause 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. Useputs({})orputs(Hash.new)to print a hash. -
A first argument that starts like an operator is ambiguous.
puts -1,p x -1(withxa local variable) andshow *listall 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?istruefor a book without an author, because after the skipped call the.nil?runs on thenilresult:&.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 asLint/SafeNavigationChain. -
&.is also valid on the left of an assignment (book.author&.name = "x", which does nothing when the author isnil) and with operator assignment (a&.count += 1). -
&.hides where thenilcame from. Use it wherenilis a documented possibility, not to silence a bug; the alternatives areHash#fetch, acase/inpattern (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 |
|
Matched to parameters left to right. |
Keyword |
|
Matched by name. Written as a braceless list of |
Hash (braced) |
|
One ordinary positional argument that happens to be a Hash. |
Block |
|
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>'
Source: Ruby 4.0 NEWS: ErrorHighlight and ArgumentError
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
Hashis unpacked into its[key, value]pairs, and aRangeinto its elements; -
*niland*(x if cond)expand to nothing, which gives conditional arguments without a helper (call(*(extra if verbose))); -
Kernel#Arrayis the explicit counterpart.Array(nil)is[],Array([1])is[1]andArray("a")is["a"]. Where the splat only triesto_a,Array()triesto_aryfirst and thento_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 giveswrong 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
[[], {}]
[[], {}]
[[], {}]
{}
Source: Ruby 4.0 NEWS: Language changes and NilClass#to_a
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
Source: Ruby documentation: Calling Methods, Unpacking Keyword Arguments and Ruby documentation: Methods
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 |
|---|---|---|
|
the braceless keywords are collected as a Hash and passed positionally |
the same |
|
|
converted to keywords |
|
keywords |
keywords |
|
|
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}
Source: Ruby 3.0 NEWS: Language changes
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
*argslost keywords unless it was marked withruby2_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
Source: Ruby documentation: Data
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_missingandrespond_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
newand 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 baresuperto 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, ¬e)
@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, &blockinstead. -
A call that forwards
...must not drop the parentheses.puts ...at the end of a line is parsed as the endless rangeputs()...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
-
In
Lend.call(book, days: 21)the forwarded keywords reachinitializeunchanged; the only cost over writing the parameters out is that the signature no longer documents them. Document forwarding methods (# @paramwith YARD, see Documentation with RDoc and YARD) or type them in RBS (Type Checking with RBS, TypeProf, Steep and Sorbet). -
The delegation can instead use the standard library:
ForwardableandSimpleDelegatorare on Modules and Mixins.
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 |
|
|
Ruby 3.1 |
|
|
Ruby 3.2 |
|
|
Source: Ruby 3.1 NEWS, Ruby 3.2 NEWS and Ruby documentation: Methods
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, :**]]
Source: Ruby documentation: Methods
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 withit, with_1or with a named parameter are fine, as infind_all_by. -
RuboCop’s
Style/ArgumentsForwardingsuggests the anonymous forms when a rest, keyword rest or block parameter is named but only forwarded, andNaming/BlockForwardingprefers&to&blockwhen 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 callf(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
Source: Ruby documentation: Methods
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 |
|---|---|
|
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 |
|
No arguments at all. The method’s block is still passed implicitly. |
|
Exactly the listed arguments. The block is passed implicitly as well; |
# frozen_string_literal: true
module Shelf
class Lend
attr_reader :book, :days, :reader
def initialize(book, days: 14, reader: "anonymous", ¬e)
@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)
Source: Ruby documentation: Methods
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.
-
&:nameisSymbol#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?istrue), has arity-2and takes the receiver as the first argument and any more as arguments to the method; -
&method(:name)turns aMethodobject into a block (Method#to_proc), which is a lambda too; -
any object that defines
to_procworks, 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
Source: Ruby documentation: Calling Methods, Proc to Block Conversion, Symbol#to_proc and Method#to_proc
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
Source: Object#public_send, Object#send and Method
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 |
Ruby 3.3 shows the receiver ( |
Hashes printed as |
Ruby 3.4 prints |
Double splat and keyword arguments covered, but without value omission, without anonymous |
Value omission (3.1), anonymous block (3.1), anonymous rest and keyword rest (3.2), |
|
It exists since Ruby 1.8.7, and since Ruby 3.0 the proc it returns is a lambda. |
Blocks written with |
Ruby 3.4 adds |
Source: Ruby 4.0 NEWS, Ruby 3.4 NEWS and Ruby 3.1 NEWS
Common errors and warnings
| Message | Usual cause |
|---|---|
|
Wrong positional count. In 4.0 the report also shows the call and the definition. A |
|
A required keyword is absent, or a keyword name is misspelled (or the receiving method has no |
|
The method was defined with |
|
A |
|
A splat of an object whose |
|
|
|
A |
|
A private method called with an explicit receiver other than |
|
|
|
|
|
A splat, |
|
A |
|
A block was given to a method that never uses it. |
Source: ArgumentError, NoMethodError and TypeError
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 |
|
Ruby 3.0 |
Keyword arguments separated from positionals (Feature #14183); leading arguments before |
Ruby 3.1 |
Value omission |
Ruby 3.2 |
Anonymous |
Ruby 3.3 |
|
Ruby 3.4 |
|
Ruby 4.0 |
|
Ruby 4.1 (upcoming) |
|
Source: Ruby 4.0 NEWS and Ruby 3.2 NEWS
References
-
Ruby documentation: Methods (parameters, argument forwarding,
super) -
Ruby News: Separation of positional and keyword arguments in Ruby 3.0
-
RuboCop: Style/ArgumentsForwarding, Naming/BlockForwarding and Lint/SafeNavigationChain