Methods and Parameters
|
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); features newer than Ruby 3.0 name the version that introduced them.
A method is the unit of behaviour in Ruby: every operation, from 1 + 2 to Catalog#add, is a method call, and defining
a method is an ordinary expression that returns a Symbol. This page covers the definition side in full: how def
works, the names a method may have, the seven kinds of parameters and the single order in which they must appear, how
default values are evaluated, what changed when Ruby 3.0 separated positional from keyword arguments, how the body of a
method can rescue and ensure without a begin, how super and the method_added hook behave, and how to design
small methods with clear signatures.
The calling side (receivers, parentheses, splats at the call site, value omission and ... forwarding) is
Calling Methods and Argument Forwarding,
blocks, procs and lambdas are Blocks, Procs and Lambdas,
and the rules that decide which method runs are in
Method Lookup and super. Visibility is covered in
Visibility and Encapsulation, the exception classes
raised by bad calls in Exceptions and Error Handling,
and generating methods at run time in
Dynamic Dispatch and Method Definition.
Methods that live inside classes are introduced in Classes and Objects.
For how other languages declare functions, see
Go functions and
Rust functions.
Every example on this page was syntax-checked with ruby -wc, run with ruby -w on Ruby 4.0.7 and linted with
RuboCop 1.92 using the configuration of the running shelf gem (target Ruby 4.0, double-quoted strings). The output
shown in # => comments, and in the output blocks, is the real output. The examples reuse the names of the shelf
library catalogue used throughout this section (Shelf, Book, Isbn, Loan, Catalog); each file is
self-contained so it can be run on its own.
Defining a method
def, implicit return and return values
A method definition is def, a name, an optional parameter list, a body and end. The value of the last expression
evaluated in the body is the return value, so an explicit return is only needed to leave early. return with several
values (return 0, 0) packs them into an Array, and a bare return returns nil. Because the result is an ordinary
Array, callers can unpack it with multiple assignment (euros, rest = fine.split).
The def keyword is itself an expression and evaluates to the method name as a Symbol (since Ruby 2.1). That is what
makes private def helper and memoize def expensive work: the modifier receives the Symbol that def returned.
Redefining a method is not an error. def simply replaces the previous definition, silently unless warnings are
enabled with -w, which is how monkey patching works and why it deserves care (see
Refinements and Monkey Patching).
The next file defines a small Fine class in the shelf namespace and exercises everything above, together with the
operator methods, the unary minus and the element reference covered in the next sections.
# frozen_string_literal: true
module Shelf
# A late fee, in cents.
class Fine
attr_reader :cents
def initialize(cents)
@cents = cents
end
# Implicit return: the value of the last expression.
def zero? = cents.zero?
# Operator methods are ordinary methods with special names.
def +(other) = Fine.new(cents + other.cents)
def -@ = Fine.new(-cents)
def <=>(other) = cents <=> other.cents
# Several return values are packed into one array.
def split
return 0, 0 if zero?
[cents / 100, cents % 100]
end
def to_s = split.then { |euros, rest| format("%<euros>d.%<rest>02d", euros:, rest:) }
# An assignment method: `fine.cents = 5` always evaluates to 5.
def cents=(value)
@cents = Integer(value)
:ignored
end
# Element reference and assignment take any number of arguments.
def [](index) = to_s[index]
end
end
fine = Shelf::Fine.new(250)
fine.to_s # => "2.50"
fine.split # => [2, 50]
euros, cents = fine.split
[euros, cents] # => [2, 50]
(fine + Shelf::Fine.new(75)).to_s # => "3.25"
(-fine).cents # => -250
fine[0] # => "2"
[fine.cents = 300, fine.send(:cents=, 400)] # => [300, :ignored]
fine.cents # => 400
fine.zero? # => false
Shelf::Fine.instance_method(:+).owner # => Shelf::Fine
Source: Ruby 4.0 documentation: Methods
Endless methods (Ruby 3.0)
A method whose body is a single expression can be written without end as def name(params) = expression. The form
was added in Ruby 3.0, and Ruby 3.1 allowed a command call as the body, such as def greet = puts "hi". The rules that
trip people up:
-
The parentheses around the parameters are mandatory when there are any:
def add_one(value) = value + 1is valid,def add_one value = value + 1is a syntax error. -
A setter cannot be endless.
def value=(v) = vis rejected with "a setter method cannot be defined in an endless method definition", because the return value of an assignment method is ignored anyway. -
Endless is a syntactic choice, not a different kind of method. It can have defaults, keywords, a block parameter and
super, and it appears inMethod#parameterslike any other. -
private def name = expressionparses in Ruby 4.0 (the Ruby 3.1 release notes say it did not at the time), so a visibility modifier and an endless method combine. -
Keep endless methods to one short expression. Anything that needs a rescue clause, a guard or a second statement belongs in a normal
def.
The syntax errors are shown with their real parser messages in Syntax errors and warnings below.
Method names
A method name starts with a letter, an underscore or a character outside ASCII (def こんにちは is legal; programs must
be written in an ASCII-compatible encoding such as UTF-8), and may contain letters, digits and underscores. By
convention words are separated with underscores (snake_case), and a method name never starts with a capital letter
unless it deliberately mimics a constant, as Kernel#Integer and Kernel#Array do.
The last character can be a suffix with a convention attached:
| Suffix | Convention |
|---|---|
|
A predicate: the method answers a question. By convention it returns a truthy or falsy value, not necessarily
|
|
The "dangerous" twin of a method without the bang, usually one that changes the receiver or raises instead of
returning a value. The bang is a warning label rather than a guarantee: |
|
An assignment method (a setter). |
# frozen_string_literal: true
# By convention `?` methods answer a question and `!` methods are the more
# dangerous twin of a method without the bang.
" padded ".strip # => "padded"
(+"clean").strip! # => nil
[3, 1, 2].sort! # => [1, 2, 3]
[1, 2, 3].uniq! # => nil
5.nonzero? # => 5
0.nonzero? # => nil
"shelf".start_with?("sh") # => true
%w[a b].index("b") # => 1
{ a: 1 }.key?(:a) # => true
:title.respond_to?(:upcase) # => true
Operator methods
Most operators are methods with special names, so any class can define them. Each binary operator takes exactly one
argument, the precedence of an operator cannot be changed, and != and !~ are derived from == and =~ unless
defined. The operators that can be defined as methods are:
| Operators | Meaning |
|---|---|
|
Arithmetic: add, subtract, multiply, divide, modulo, power |
|
Equality, case equality ( |
|
Ordering; |
|
Shifts and append, bitwise and, or and exclusive or |
|
Element reference and assignment, with any number of arguments |
|
Unary plus and minus (the |
&&, ||, and, or, defined? and the assignment operators are not methods and cannot be redefined;
a += 1 is expanded to a = a + 1 and so uses +. The example above defines +, -@, <=> and []. Defining an
operator with a surprising meaning is possible and almost always a mistake; a + that does not add confuses readers.
Parameters
The seven kinds and their order
Ruby has seven kinds of parameter. They must appear in exactly this order, and the parser rejects anything else with "unexpected parameter order":
| Order | Kind | Syntax | Method#parameters tag |
|---|---|---|---|
1 |
Required |
|
|
2 |
Optional (with a default) |
|
|
3 |
Rest (splat) |
|
|
4 |
Post-required (after the optional or rest parameters) |
|
|
5 |
Keyword, required or optional (in any order between themselves) |
|
|
6 |
Keyword rest, or the |
|
|
7 |
Block |
|
|
The names must be unique (the exception is names that start with an underscore, so def ignore(_, _) is valid), a
parameter cannot be a constant or an instance variable, and keyword parameters may use reserved words such as if: or
class:; the value is then read with binding.local_variable_get(:if). Only one rest parameter is allowed. Optional
parameters must be grouped together: def f(a = 1, b, c = 1) is a syntax error, while def f(a = 1, b = 2, c) is fine
and def f(a, b = 2, *rest, z) shows required, optional, rest and post-required together.
The file below defines one method with every kind, passes it a mixture of arguments, and then asks Ruby to describe
the signature with Method#parameters and Method#arity:
# frozen_string_literal: true
module Shelf
# Every kind of parameter, in the only order Ruby accepts.
def self.register(isbn, title = "untitled", *tags, edition, format:, lang: "en", **extra, ¬e)
note&.call(isbn)
{ isbn:, title:, tags:, edition:, format:, lang:, extra: }
end
end
isbn = "9780132350884"
Shelf.register(isbn, "Clean Code", :ruby, :craft, 1, format: :paper, shelf: "A3")
# => {isbn: "9780132350884", title: "Clean Code", tags: [:ruby, :craft], edition: 1,
# format: :paper, lang: "en", extra: {shelf: "A3"}}
Shelf.register(isbn, 2, format: :ebook) { |value| value }
# => {isbn: "9780132350884", title: "untitled", tags: [], edition: 2,
# format: :ebook, lang: "en", extra: {}}
Shelf.method(:register).parameters
# => [[:req, :isbn], [:opt, :title], [:rest, :tags], [:req, :edition], [:keyreq, :format],
# [:key, :lang], [:keyrest, :extra], [:block, :note]]
Shelf.method(:register).arity # => -4
Source: Method#parameters
The Hash that register returns uses the Ruby 3.1 shorthand { isbn:, title: }, which stands for { isbn: isbn, title: title }.
The edition parameter is post-required: in the second call the lone 2 is not the title but the edition, because required
parameters are served before optional ones.
How the arguments are matched to the parameters is mechanical: required parameters are filled first (from the left for
the leading ones, from the right for the post-required ones), optional parameters take what is left from the left, and the rest parameter
collects the remainder. If the method has keyword parameters, a trailing Hash that was written as key: value pairs at
the call (or passed with **) is bound to them. The block, if any, is never counted among the arguments.
Required, optional and post-required parameters
A parameter with a default, title = "untitled", becomes optional. The default is an arbitrary expression that is
evaluated at call time, only when the argument is missing, and from left to right. That has four consequences, shown in the
next file: a default can use the parameters to its left (hours = days * 24), it can call methods and read constants,
a default such as [] creates a new Array on every call (unlike Python, a mutable default is not shared between calls), and
a default cannot refer to a parameter that is bound later, which fails with a NameError at call time.
# frozen_string_literal: true
# Defaults are ordinary expressions evaluated on every call that needs them.
module Shelf
DEFAULT_DAYS = 14
def self.due_in(days = DEFAULT_DAYS, hours = days * 24)
{ days:, hours: }
end
def self.collect(book, into = [])
into << book
end
# A default may call a method, so it can depend on the receiver's state.
def self.stamp(at = counter)
at
end
def self.counter
@counter = (@counter || 0) + 1
end
end
Shelf.due_in # => {days: 14, hours: 336}
Shelf.due_in(7) # => {days: 7, hours: 168}
Shelf.due_in(7, 1) # => {days: 7, hours: 1}
Shelf.collect("a").equal?(Shelf.collect("b")) # => false
Shelf.collect("a") # => ["a"]
[Shelf.stamp, Shelf.stamp, Shelf.stamp(99), Shelf.stamp] # => [1, 2, 99, 3]
# A default cannot look ahead to a parameter that is bound later.
def broken(first = second, second = 1) = [first, second]
begin
broken
rescue NameError => e
e.message # => "undefined local variable or method 'second' for main"
end
If a method has post-required parameters, the optional ones are filled only when there are enough arguments for the required ones,
which gives the surprising but consistent results of fill(1, 2) and fill(1, 2, 3) in the next section. Prefer
keyword arguments over several optional positional parameters: lend(book, days: 7) explains itself at the call
site, lend(book, 7) does not.
Rest parameters and array decomposition
A parameter prefixed with * collects the remaining positional arguments into an Array. It can sit at the start,
in the middle (with required parameters on both sides), or at the end. A bare * accepts and ignores the arguments, or
passes them on unchanged with gather(*) (anonymous rest forwarding, Ruby 3.2). The rest parameter appears before any
keyword parameters.
A subtle rule: when the method declares no keyword parameters, keywords written at the call are collected as a trailing
Hash by the rest parameter (gather(1, a: 2) gives [1, {a: 2}]). When it does declare keywords, an unknown key
raises ArgumentError instead. This is the behaviour that Ruby 3.0 stabilised and is the subject of
the section on the 3.0 separation below.
A parameter written in parentheses decomposes one argument with to_ary, in the same way as multiple assignment: the
argument is split into the names inside the parentheses, extra elements are dropped, and a non-Array argument goes to
the first name while the others become nil. A * inside the parentheses collects the rest. The decomposed parameter
counts as one positional parameter and appears as [:req] without a name in Method#parameters. Define to_ary on a class only if it
can truly be used in place of an Array.
# frozen_string_literal: true
# Splats, post-required parameters and array decomposition.
def slice_of(first, *middle, last) = { first:, middle:, last: }
slice_of(1, 2) # => {first: 1, middle: [], last: 2}
slice_of(1, 2, 3, 4) # => {first: 1, middle: [2, 3], last: 4}
# Optional parameters are filled before the splat, after the required ones.
def fill(a, b = :b, *rest, z) = [a, b, rest, z]
fill(1, 2) # => [1, :b, [], 2]
fill(1, 2, 3) # => [1, 2, [], 3]
fill(1, 2, 3, 4, 5) # => [1, 2, [3, 4], 5]
# Parentheses decompose one argument with to_ary, like multiple assignment.
def title_and_author((title, author), year) = "#{title} (#{author}, #{year})"
title_and_author(%w[Dune Herbert], 1965) # => "Dune (Herbert, 1965)"
title_and_author("Dune", 1965) # => "Dune (, 1965)"
def head_and_tail((head, *tail)) = [head, tail]
head_and_tail([1, 2, 3]) # => [1, [2, 3]]
# Without keyword parameters a trailing keyword list becomes a Hash.
def gather(*args) = args
gather(1, a: 2) # => [1, {a: 2}]
def gather_with_keyword(*args, only: nil) = [args, only]
begin
gather_with_keyword(1, other: 3)
rescue ArgumentError => e
e.message # => "unknown keyword: :other"
end
# Bare `*` accepts and ignores the arguments, or passes them on.
def ignore(*) = :ignored
def forward_positional(*) = gather(*)
ignore(1, 2, 3) # => :ignored
forward_positional(1, 2, 3) # => [1, 2, 3]
Keyword parameters
A keyword parameter is declared as name: (required) or name: default (optional). Callers pass the keywords in any
order, and a call that omits a required keyword or passes an unknown one raises an ArgumentError that names the
keywords (missing keyword: :member, unknown keyword: :color). Required and optional keywords may be declared in any order
relative to each other. A default can use the keywords and optional parameters to its left, evaluated at call time as for positional defaults.
**opts collects every keyword that has no parameter of its own into a Hash (with Symbol keys, and since Ruby 2.7 also
with other keys). A bare ** accepts the keywords without naming them, or forwards them with other(**)
(Ruby 3.2). **nil goes the other way: it declares that the method accepts no keywords, so that a call with keywords
raises ArgumentError (no keywords accepted) instead of silently turning them into a positional Hash. It is used to
reserve the right to add keywords later without breaking callers, and it cannot be combined with other keyword
parameters. On the calling side, **nil is allowed from Ruby 3.4 and passes no keywords.
When the caller already has local variables named like the keywords, Ruby 3.1 lets the call omit the value:
Shelf.lend(book:, member:, days: 7) is short for book: book, member: member. That is the calling side and is covered in
Calling Methods and Argument Forwarding.
# frozen_string_literal: true
# Keyword parameters: required, optional, **rest and **nil.
module Shelf
def self.search(query, limit: 10, genre: nil, **filters)
{ query:, limit:, genre:, filters: }
end
def self.lend(book:, member:, days: 14) = "#{book} to #{member} for #{days} days"
def self.no_options(value, **nil) = value
end
Shelf.search("ruby") # => {query: "ruby", limit: 10, genre: nil, filters: {}}
Shelf.search("ruby", genre: :tech, limit: 5, year: 2025)
# => {query: "ruby", limit: 5, genre: :tech, filters: {year: 2025}}
Shelf.search("ruby", **{ "lang" => "en" })
# => {query: "ruby", limit: 10, genre: nil, filters: {"lang" => "en"}}
Shelf.lend(member: "ana", book: "Dune") # => "Dune to ana for 14 days"
book = "Dune"
member = "ana"
Shelf.lend(book:, member:, days: 7) # => "Dune to ana for 7 days"
calls = [
-> { Shelf.lend(book: "Dune") },
-> { Shelf.lend },
-> { Shelf.lend(book: "Dune", member: "ana", color: :red) },
-> { Shelf.lend("Dune", "ana") },
-> { Shelf.no_options(1, verbose: true) }
]
messages = calls.map do |call|
call.call
rescue ArgumentError => e
e.message
end
pp messages
A method without keyword parameters and without **nil that receives key: value arguments gets them as a final
positional Hash (meth(a: 1) binds {a: 1} to arg). That is still true in Ruby 4.0, which is why the 3.0 rules below
are worth learning precisely. The same call with an explicit brace Hash, meth({a: 1}), is one positional argument in
every version.
The block parameter
The last parameter may be &name; it receives the block of the call as a Proc (or nil if there is none). It is
mostly used to pass the block on to another method. Ruby 3.1 added the anonymous form def each(&) = @items.each(&) for
the case where the block is only forwarded. When the method just calls the block, prefer yield with block_given?, which does not allocate
a Proc, and enum_for(__method__) to return an Enumerator when no block is given (the pattern used by the shelf
Catalog#each).
# frozen_string_literal: true
# Blocks are an implicit last argument: yield, block_given? and &block.
module Shelf
def self.each_title(titles)
return enum_for(__method__, titles) unless block_given?
titles.each { |title| yield title.upcase, title.size }
end
def self.capture(&block) = block
def self.pass_along(titles, &) = titles.map(&)
end
Shelf.each_title(%w[Dune Emma]).to_a # => [["DUNE", 4], ["EMMA", 4]]
Shelf.each_title(%w[Dune Emma]) { |title, size| puts "#{title}: #{size}" }
captured = Shelf.capture { |a, b| [a, b] }
[captured.class, captured.arity, captured.lambda?] # => [Proc, 2, false]
Shelf.pass_along(%w[Dune Emma], &:size) # => [4, 4]
Shelf.capture # => nil
Run, the method prints the two upper-cased titles with their length:
DUNE: 4
EMMA: 4
A block is created by the caller; the Proc that &block returns is a regular (non-lambda) proc with its own
arity, which is why captured.arity above is 2 for a block with two parameters. The full semantics of yield, return
inside blocks, Method#to_proc and Symbol#to_proc (the &:size argument) are in
Blocks, Procs and Lambdas.
Anonymous parameters and forwarding
Three shorthand forms avoid naming a parameter whose only job is to be passed on. Ruby 2.7 added ... (all
arguments and the block, with leading arguments allowed from 3.0), Ruby 3.1 the anonymous block &, and Ruby 3.2 the
anonymous * and **. A method can use *, ** and & in the same parameter list. ... stands for all three, so it cannot be combined
with a rest parameter or a block (def f(*, ...) is rejected with "… after rest argument"), and ... is only valid in a method that declares it (def f(*) = g(...) is a syntax error,
shown in the diagnostics below). Method#parameters reports all of them under the placeholder names :*, :** and :&:
# frozen_string_literal: true
# Method#parameters reports each parameter kind, including anonymous ones.
def positional(a, b = 1, *c, d) = [a, b, c, d]
def keywords(e:, f: 1, **g) = [e, f, g]
def anonymous(*, **, &) = nil
def forwarding(...) = nil
def no_keywords(**nil) = nil
def decomposed((a, b), c) = [a, b, c]
parameters = %i[positional keywords anonymous forwarding no_keywords decomposed].to_h do |name|
[name, method(name).parameters]
end
pp parameters
Output:
{positional: [[:req, :a], [:opt, :b], [:rest, :c], [:req, :d]],
keywords: [[:keyreq, :e], [:key, :f], [:keyrest, :g]],
anonymous: [[:rest, :*], [:keyrest, :**], [:block, :&]],
forwarding: [[:rest, :*], [:keyrest, :**], [:block, :&]],
no_keywords: [[:nokey]],
decomposed: [[:req], [:req, :c]]}
Ruby 4.0 changed one forwarding detail: *nil at a call site no longer calls nil.to_a, in the same way that
**nil never called nil.to_hash.
Method#parameters and arity
Method#parameters (and UnboundMethod#parameters, Proc#parameters) returns an Array of [kind, name] pairs. The kinds are
:req, :opt, :rest, :keyreq, :key, :keyrest, :block and :nokey. A non-lambda proc reports its required parameters as
:opt, and Proc#parameters(lambda: true) (Ruby 3.2) asks for the lambda view. Frameworks use this to adapt a callback to the number
of arguments it accepts.
Method#arity is a single number summarising the same information:
-
n(zero or more) when the method takes exactlynpositional arguments and no optional ones; -
-n-1when it takesnrequired arguments plus optional or rest arguments (-1fordef f(*rest),-2fordef f(a, b = 1),-3fordef f(a, *rest, z)); -
keywords count as one extra argument, which is mandatory if any keyword is required:
def f(a, k:)has arity2,def f(k:)has1,def f(k: 1)has-1,def f(a, k: 1)has-2; -
a block parameter never counts, and
...has arity-1.
For a proc (not a lambda), optional arguments do not make the arity negative, so proc { |a, b = 1| }.arity is 1
while lambda { |a, b = 1| }.arity is -2.
# frozen_string_literal: true
# Method#arity for the common shapes.
module Arities
def none; end
def one(a) = a
def two(a, b) = [a, b]
def optional(a, b = 1) = [a, b]
def splat(*rest) = rest
def post(a, *rest, z) = [a, rest, z]
def required_keyword(a:) = a
def optional_keyword(a: 1) = a
def mixed(a, b:) = [a, b]
def mixed_optional(a, b: 1) = [a, b]
def keyrest(**opts) = opts
def forwarding(...) = nil
def block_only(&block) = block
end
arities = Arities.instance_methods(false).sort.to_h { [it, Arities.instance_method(it).arity] }
pp arities
Output:
{block_only: 0,
forwarding: -1,
keyrest: -1,
mixed: 2,
mixed_optional: -2,
none: 0,
one: 1,
optional: -2,
optional_keyword: -1,
post: -3,
required_keyword: 1,
splat: -1,
two: 2}
Source: Method#arity
What ArgumentError looks like
Wrong calls are reported as an ArgumentError whose message states what was given and what was expected. The
backtrace points to the method definition line first (the Class#initialize frame for new), then to the call:
| Call | Message |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
An undefined method or variable raises NoMethodError or NameError with the 3.4 message format, which names the receiver by
its class: undefined method 'foo' for nil, undefined method 'zork' for an instance of Object,
undefined local variable or method 'second' for main, private method 'helper' called for an instance of String.
Messages from before Ruby 3.4 quoted the method with a backtick and ended with for nil:NilClass; code that matches
on the text of these messages needs updating.
Positional and keyword arguments since Ruby 3.0
Until Ruby 2.6 a method that accepted keywords also accepted a final Hash and converted between the two, which caused ambiguous calls that were fixed in surprising ways. Ruby 2.7 printed warnings for every place where the conversion happened and Ruby 3.0 removed it: keyword arguments are now strictly separate from positional arguments. The rules are short:
| Situation | Ruby 3.0 and later |
|---|---|
A method with keyword parameters is called with a Hash |
The Hash is one positional argument. |
A method without keyword parameters is called with |
The pairs become one trailing positional Hash. |
A method accepts an optional Hash and |
Braces make a positional Hash ( |
|
Passes no arguments at all (it does not pass an empty Hash), so |
A rest parameter delegates with |
The final keyword Hash is just a Hash unless the method is marked with |
A non-lambda proc whose only parameters are one rest parameter and keywords |
No longer autosplats an Array argument (3.0); a proc with a single positional parameter and keywords no longer autosplats either (3.2). |
The next file shows these rules at work, with the real error messages of the calls that no longer work:
# frozen_string_literal: true
# Since Ruby 3.0 positional and keyword arguments are separate.
def with_keywords(title:, year: nil) = [title, year]
def with_hash(options) = options
def with_both(options = {}, **keywords) = [options, keywords]
def with_rest(*args) = args
attrs = { title: "Dune", year: 1965 }
splat = [attrs]
with_keywords(**attrs) # => ["Dune", 1965]
with_hash(title: "Dune") # => {title: "Dune"}
with_hash(**attrs) # => {title: "Dune", year: 1965}
with_both({ title: "Dune" }) # => [{title: "Dune"}, {}]
with_both(title: "Dune") # => [{}, {title: "Dune"}]
with_rest(1, title: "Dune") # => [1, {title: "Dune"}]
calls = {
hash_to_keywords: -> { with_keywords(attrs) },
hash_and_keywords: -> { with_keywords({ title: "Dune" }, year: 1965) },
splatted_hash: -> { with_keywords(*splat) },
empty_to_required: -> { with_hash(**{}) }
}
errors = calls.transform_values do |call|
call.call
rescue ArgumentError => e
e.message
end
pp errors
Output:
{hash_to_keywords:
"wrong number of arguments (given 1, expected 0; required keyword: title)",
hash_and_keywords:
"wrong number of arguments (given 1, expected 0; required keyword: title)",
splatted_hash:
"wrong number of arguments (given 1, expected 0; required keyword: title)",
empty_to_required: "wrong number of arguments (given 0, expected 1)"}
Source: Ruby 3.0.0 NEWS
The 2.7 deprecation warnings are gone, so there is no way to opt back in; the only compatibility switch left is
ruby2_keywords for delegators, described next. The reasoning for the change is given in the official article
Separation of positional and keyword arguments in Ruby 3.0.
Delegating without losing the keyword distinction
A method that gathers *args and calls target.method(*args) used to forward keywords transparently, because the keywords
were just a trailing Hash that Ruby turned back into keywords. After Ruby 3.0 the Hash stays positional and the call fails (the Broken class
below). There are three fixes, in order of preference:
-
declare the keywords explicitly,
def lend(*args, **options, &block)(or its anonymous formdef lend(*, **, &), Ruby 3.2); -
use
..., which forwards everything, including a leading argument you inspect:def lend(book, ...)(Ruby 3.0); -
mark the method with
ruby2_keywords, which flags the trailing Hash so that a later*argssplat turns it back into keywords. This is the bridge that libraries use to support Ruby 2.6 and 3.x with one method, and it also exists asProc#ruby2_keywords.
# frozen_string_literal: true
# Delegation without losing the keyword/positional distinction.
class Recorder
attr_reader :log
def initialize = @log = []
def lend(book, member:, days: 14)
@log << [book, member, days]
end
end
# Modern forms: anonymous forwarding with its own default, or `...` with a leading argument.
class Explicit
def initialize(target) = @target = target
def lend(book, *, days: 7, **, &) = @target.lend(book, *, days:, **, &)
end
class Shorthand
def initialize(target) = @target = target
def lend(book, ...) = @target.lend(book.strip, ...)
end
# ruby2_keywords is the Ruby 2.7 bridge for code that only gathers *args.
class Legacy
def initialize(target) = @target = target
ruby2_keywords def lend(*, &)
@target.lend(*, &)
end
end
logs = [Explicit, Shorthand, Legacy].map do |klass|
recorder = Recorder.new
klass.new(recorder).lend("Dune", member: "ana")
recorder.log.first
end
pp logs
# Without ruby2_keywords the keywords arrive as a last positional Hash.
class Broken
def initialize(target) = @target = target
def lend(*, &) = @target.lend(*, &)
end
begin
Broken.new(Recorder.new).lend("Dune", member: "ana")
rescue ArgumentError => e
e.message # => "wrong number of arguments (given 2, expected 1; required keyword: member)"
end
Source: Module#ruby2_keywords
Explicit forwards anonymously and adds its own default days:, Shorthand inspects the leading argument and forwards the rest with
..., Legacy relies on ruby2_keywords, and Broken shows the error message of the delegator that does neither. Run, the three
working delegators record the call, and the last one fails with the message in the comment of the file:
Output:
[["Dune", "ana", 7], ["Dune", "ana", 14], ["Dune", "ana", 14]]
Source: Module#ruby2_keywords
ruby2_keywords is a transitional tool. It is deprecated in the development version that will become Ruby 4.1 (the
development NEWS lists Module#ruby2_keywords, top-level ruby2_keywords and Proc#ruby2_keywords as deprecated, with
removal planned for Ruby 4.4), so new code should use ... or explicit forwarding, and this page does not rely on
the deprecation in 4.0. A hash that carries the flag can be recognised with Hash.ruby2_keywords_hash?.
A last property worth knowing: a **opts parameter is always a fresh Hash, so mutating it never affects the caller’s
Hash, while a Hash passed positionally is the same object:
# frozen_string_literal: true
# **opts is a fresh Hash on every call: the caller's Hash is never changed.
def tweak(**opts)
opts[:seen] = true
opts
end
original = { title: "Dune" }
result = tweak(**original)
[original, result, result.equal?(original)] # => [{title: "Dune"}, {title: "Dune", seen: true}, false]
# A Hash passed as a positional argument IS shared, so mutating it leaks out.
def tweak_positional(options)
options[:seen] = true
options
end
shared = { title: "Dune" }
tweak_positional(shared).equal?(shared) # => true
shared.to_h # => {title: "Dune", seen: true}
Method bodies
rescue, else, ensure and retry without begin
A method body is an implicit begin block. rescue, else and ensure can follow the body directly, and
retry inside a rescue clause runs the body again. ensure runs once when the method finally leaves, however it leaves,
including after several retries; its own value is discarded unless it executes return, in which case that return value wins and an
in-flight exception is silently swallowed (a classic bug, flagged by RuboCop’s Lint/EnsureReturn). For a rescue of only part of the body, use an explicit
begin/end. Inside a rescue clause Ruby sets cause automatically to the exception being handled;
the example passes cause: e explicitly to document the intent, and it is required when raising outside a rescue clause.
# frozen_string_literal: true
module Shelf
class Error < StandardError; end
class InvalidIsbn < Error; end
# A method body is an implicit begin block: rescue, else, ensure
# and retry work without an explicit `begin`.
class Importer
attr_reader :trail
def initialize(failures: 0)
@failures = failures
@attempts = 0
@trail = []
end
def import(raw)
@attempts += 1
@trail << :try
raise InvalidIsbn, "flaky source" if @attempts <= @failures
Integer(raw)
rescue InvalidIsbn
@trail << :retry
retry if @attempts < 3
raise
rescue ArgumentError, TypeError => e
@trail << :bad_input
raise InvalidIsbn, "not a number: #{raw.inspect}", cause: e
else
@trail << :ok
:imported
ensure
@trail << :done
end
end
end
importer = Shelf::Importer.new(failures: 2)
importer.import("9780132350884") # => :imported
importer.trail # => [:try, :retry, :try, :retry, :try, :ok, :done]
begin
Shelf::Importer.new(failures: 5).import("9780132350884")
rescue Shelf::InvalidIsbn => e
e.message # => "flaky source"
end
begin
Shelf::Importer.new.import("x1")
rescue Shelf::InvalidIsbn => e
[e.message, e.cause.class] # => ["not a number: \"x1\"", ArgumentError]
end
# The value of an `ensure` clause is discarded, unless it executes `return`.
def ensure_value(log)
:body
ensure
log << :cleanup
end
cleanup_log = []
ensure_value(cleanup_log) # => :body
cleanup_log.to_a # => [:cleanup]
The second block in the file shows the discarded ensure value: ensure_value returns :body, and the ensure clause only ran its side effect on the log.
The exception hierarchy and the rules for what to rescue are in
Exceptions and Error Handling.
return in methods, procs and lambdas
return leaves the method, even from inside a block: first_even returns from itself while each is iterating. Inside a lambda,
return leaves only the lambda. A proc (or block) created in a method that has already returned cannot return from it, and
raises LocalJumpError (unexpected return). The same rules for break and next are drawn as a diagram in
Blocks, Procs and Lambdas. A bare return returns nil, and return a, b returns [a, b].
# frozen_string_literal: true
# return, break and next differ between a method, a proc and a lambda.
def first_even(numbers)
numbers.each { |n| return n if n.even? }
nil
end
first_even([1, 3, 4, 5]) # => 4
def lambda_return
early = lambda do |value|
return :from_lambda if value
:not_reached
end
[early.call(true), :method_continues]
end
lambda_return # => [:from_lambda, :method_continues]
def orphan = proc { return :from_method }
begin
orphan.call
rescue LocalJumpError => e
[e.class, e.message] # => [LocalJumpError, "unexpected return"]
end
# Several return values form an Array; a bare `return` returns nil.
def bounds(list)
return if list.nil?
return 0, 0 if list.empty?
list.minmax
end
bounds([3, 1, 2]) # => [1, 3]
bounds([]) # => [0, 0]
bounds(nil) # => nil
low, high = bounds([3, 1, 2])
[low, high] # => [1, 3]
The assignment-method rule from the first example applies here too: the value a setter returns is ignored when it is called with the
assignment syntax, so return in a setter is harmless but pointless.
__method__, __callee__, aliases and the method_added hook
Inside a method, __method__ returns the Symbol of the method being defined and __callee__ the name it was called by;
the two differ for an alias. They are what enum_for(__method__) uses. caller_locations(1, 1).first.label names the calling method.
When a method is defined, Module#method_added is called on the class (and singleton_method_added on the object
for a singleton method); method_removed and method_undefined mirror it for remove_method and undef_method. Every way of
creating a method triggers it: def, define_method, attr_accessor, alias. The hook must call super, otherwise any other module in the chain that
defines it is silenced. alias (a keyword, the old and new names are not Symbols) and Module#alias_method (a method, usable in
class_eval and with computed names) copy a method under a second name; Method#original_name keeps the first. The example prints what the hook observed:
# frozen_string_literal: true
# `def` is an expression that returns the method name as a Symbol.
# The Module hooks tell a class which methods are being added and removed.
class Audited
@added = []
@removed = []
class << self
attr_reader :added, :removed
def method_added(name)
@added << name
super
end
def method_removed(name)
@removed << name
super
end
end
RETURNED = [def checkout = :out, private(def secret = :hidden)].freeze
attr_accessor :note
define_method(:dynamic) { :made }
alias borrow checkout
remove_method :checkout
def who_am_i = [__method__, __callee__]
alias alias_of_who who_am_i
end
Audited::RETURNED.to_a # => [:checkout, :secret]
Audited.added # => [:checkout, :secret, :note, :note=, :dynamic, :borrow, :who_am_i, :alias_of_who]
Audited.removed # => [:checkout]
Audited.new.who_am_i # => [:who_am_i, :who_am_i]
Audited.new.alias_of_who # => [:who_am_i, :alias_of_who]
Audited.instance_method(:borrow).name # => :borrow
Audited.instance_method(:borrow).original_name # => :checkout
Audited.private_instance_methods(false) # => [:secret]
Source: Module#method_added
Two lines are worth reading closely. Audited::RETURNED is the pair of Symbols that the two def expressions returned (the private(...) call returns its
argument since Ruby 3.1). And alias_of_who reports [:who_am_i, :alias_of_who]: __method__ is the original name, __callee__ the alias.
Visibility is covered in
Visibility and Encapsulation.
The forms of super
Inside a method that overrides an inherited one, super has three forms, and the difference is which arguments they pass:
-
super(implicit, "zsuper") passes the current values of the method’s own parameters, including keywords and the block. If the method reassigned a parameter before callingsuper, the new value is passed. -
super()passes no arguments (but still passes the block). -
super(a, k: 1)passes exactly what is written, andsuper(...)passes the forwarded arguments of a...method.
# frozen_string_literal: true
# The three forms of super, seen through the parameters they pass.
class Base
def lend(book, member:, days: 14) = [book, member, days]
end
class Implicit < Base
def lend(book, member:, days: 14)
book = book.upcase
super # passes the current values of book, member and days
end
end
class Explicit < Base
def lend(book, member:, days: 14)
super(book, member: member.capitalize, days: days * 2)
end
end
class Bare < Base
def lend(book, member:, days: 14)
super()
ensure
[book, member, days]
end
end
class Anonymous < Base
def lend(book, ...)
super(book.upcase, ...)
end
end
Implicit.new.lend("dune", member: "ana") # => ["DUNE", "ana", 14]
Explicit.new.lend("dune", member: "ana") # => ["dune", "Ana", 28]
Anonymous.new.lend("dune", member: "ana", days: 3) # => ["DUNE", "ana", 3]
begin
Bare.new.lend("dune", member: "ana")
rescue ArgumentError => e
e.message # => "wrong number of arguments (given 0, expected 1; required keyword: member)"
end
Source: Ruby 4.0 documentation: Methods and Modules and Classes
Implicit super is not allowed inside define_method, and in a method with anonymous parameters it forwards them. How the next
method is found is explained in
Method Lookup and super.
Top-level methods and overriding
A def outside any class defines a private method on Object, so it is callable from everywhere, including inside other classes, but
not with an explicit receiver. That is convenient in scripts and a source of namespace pollution in libraries; put library code in a
module or class. send bypasses the visibility check.
Redefinition replaces the old method. With ruby -w (or $VERBOSE = true) Ruby warns "method redefined; discarding old greet" and points to the previous
definition, which is useful in tests and CI. The next file redefines Greeter#greet from a string so that the warning is
not a RuboCop offence; run with -w, it prints the two warning lines shown.
# frozen_string_literal: true
# A `def` at the top level defines a private method on Object.
def helper = :helper
Object.private_method_defined?(:helper) # => true
Object.public_method_defined?(:helper) # => false
self.class # => Object
begin
"text".helper
rescue NoMethodError => e
e.message # => "private method 'helper' called for an instance of String"
end
"text".send(:helper) # => :helper
# Redefining a method is silent unless warnings are enabled (ruby -w).
class Greeter
def greet = "hello"
end
Greeter.class_eval <<~RUBY, __FILE__, __LINE__ + 1
def greet = "hello again"
RUBY
Greeter.new.greet # => "hello again"
$ ruby -w e14.rb
e14.rb:24: warning: method redefined; discarding old greet
e14.rb:20: warning: previous definition of greet was here
Redefining a core method such as String#to_i affects every caller in the process, the case the Ruby documentation itself
warns about. Prefer a refinement, a subclass, prepend or a new method name (see
Refinements and Monkey Patching).
Designing methods and signatures
The mechanics above leave a lot of freedom. These guidelines keep the freedom from turning into confusion:
-
Small methods with one job. A method that needs a comment to explain a section usually contains a method trying to get out. Ruby makes extracting cheap: no types to declare, and a block can often replace a helper.
-
Few positional parameters. One to three. Beyond that, use keyword parameters (
lend(book:, member:, days: 14)) or a parameter object. -
Parameter objects. Related arguments that always travel together become a value object.
Data.defineis the right tool (Ruby 3.2): immutable, with positional or keyword construction,with,to_hand pattern matching. A custominitializewith keyword defaults and validation keeps the invariant in one place (see Struct, Data and Value Objects). -
Guard clauses first. Reject bad input at the top with
raise ArgumentErrorand return early for the trivial case, so the rest of the method is the happy path, unindented. Check types sparingly: Ruby is duck typed, sorespond_to?or anis_a?check belongs at the boundary of the library, not in every method. -
Normalise with conversion methods.
Kernel#Arrayturnsnilinto[], wraps a scalar and leaves an Array alone;Integer("42"),String(x)andHash(x)are the strict conversions covered in Conversions and Coercion. -
One kind of return value. A method that returns an Array sometimes,
nilsometimes and a String on error forces every caller to check. Return an empty Array for "nothing", raise for failure, and usenilonly when absence is a valid answer. -
Name predicates and queries by their answer (
loaned?,overdue?), commands by their action (lend,return_book), and keep a setter free of surprises. Overrideto_sfor the human form andinspectfor debugging.
# frozen_string_literal: true
module Shelf
# A parameter object: related arguments travel together, with defaults and
# validation in one place.
SearchQuery = Data.define(:text, :limit, :genres) do
def initialize(text:, limit: 10, genres: [])
raise ArgumentError, "text must be a String, got #{text.class}" unless text.is_a?(String)
raise ArgumentError, "limit must be positive" unless limit.positive?
super(text: text.strip, limit:, genres: Array(genres).freeze)
end
def to_s = "#{text} (#{genres.join(', ')})"
end
# Guard clauses reject bad input first; the happy path stays unindented.
def self.search(titles, query)
return [] if titles.empty?
pattern = query.text.downcase
titles.select { it.downcase.include?(pattern) }.first(query.limit)
end
end
titles = ["Programming Ruby", "The Ruby Way", "Eloquent Ruby", "Dune"]
query = Shelf::SearchQuery.new(text: " ruby ", limit: 2, genres: :tech)
query.to_h # => {text: "ruby", limit: 2, genres: [:tech]}
Shelf.search(titles, query) # => ["Programming Ruby", "The Ruby Way"]
Shelf.search([], query) # => []
Shelf::SearchQuery.new(text: "dune").genres # => []
Array(nil) # => []
Array("one") # => ["one"]
Array(titles).equal?(titles) # => true
Array(1..3) # => [1, 2, 3]
Array({ a: 1 }) # => [[:a, 1]]
begin
Shelf::SearchQuery.new(text: :ruby)
rescue ArgumentError => e
e.message # => "text must be a String, got Symbol"
end
Source: Kernel#Array
Pass objects instead of rows of arguments, and let a method with a long signature own a single Data parameter: the call site
Shelf.search(titles, query) reads the same whether the query has two fields or twenty.
-
Duck typing makes signatures the interface. Ruby has no declared interfaces: the parameter list and the messages the body sends to each argument are the contract. A method that calls
eachon its argument accepts an Array, aCatalog, a Set or an Enumerator. Document that expectation (and type it with RBS when it matters) instead of checking classes. -
Chains and long conditions. Put each step of a method chain on its own line, starting with the dot. Since Ruby 4.0 a line that starts with
&&or||continues the previous one in the same way, so long conditions need no trailing operator. -
Comments explain why. A comment that repeats the code goes stale; one that records a decision or a constraint does not. Document the public contract of a method with RDoc or YARD comments (see Documentation with RDoc and YARD).
# frozen_string_literal: true
module Shelf
Member = Data.define(:name, :active)
# Since Ruby 4.0 a line that starts with && or || continues the previous one.
def self.lendable?(copies, member)
copies.positive?
&& member.active
&& !member.name.empty?
end
# Leading dots keep a method chain readable, one step per line.
def self.clean_titles(raw)
raw
.compact
.map(&:strip)
.reject(&:empty?)
.map(&:capitalize)
end
end
ana = Shelf::Member.new(name: "Ana", active: true)
Shelf.lendable?(2, ana) # => true
Shelf.lendable?(0, ana) # => false
Shelf.lendable?(1, ana.with(active: false)) # => false
Shelf.clean_titles([" dune ", "", nil, "EMMA"]) # => ["Dune", "Emma"]
Source: Ruby 4.0.0 NEWS
Where older books and tutorials are outdated
Several well-regarded Ruby books date from the Ruby 2.x and early 3.x era, and what they say about methods needs adjusting for Ruby 4.0:
| Older statement | Current behaviour |
|---|---|
Error messages that quote the method name with a backtick and end in |
Ruby 3.3 changed the receiver text and 3.4 the quotes: |
A Hash is converted to keywords (and the reverse) when calling |
Removed in Ruby 3.0. Use |
|
A syntax error in every version: positional parameters come before keywords. The parser says "unexpected parameter order". |
Bare |
It forwards the current values of the parameters, including keywords. In a method that changed |
|
|
|
These are conventions with exceptions: |
Only |
|
Methods written with |
Endless methods (3.0), argument forwarding with |
Subclassing the result of |
Assign the generated class to a constant instead, |
|
Ruby 3.4 prints |
Common exceptions and warnings
| Exception or warning | Typical cause |
|---|---|
|
Wrong arity, missing or unknown keyword, a validation failure raised on purpose. The message always says what was given and expected. |
|
The receiver does not respond to the method, the method is private or protected and was called with a receiver, or a |
|
A default refers to a parameter bound later, or a misspelt local variable ( |
|
An implicit conversion failed inside the body, such as |
|
A |
|
A forbidden parameter order, a duplicated name, an endless setter, or |
|
|
|
A local variable (not a parameter) is never read. Prefix it with |
|
An expression statement whose value is thrown away, usually a missing |
|
Code after a |
|
|
Syntax errors and warnings
The parser (Prism, the default since Ruby 3.4) reports forbidden parameter lists with a message pointing at the offending token. The helper below compiles each definition and extracts the message, which makes it easy to learn what each mistake looks like:
# frozen_string_literal: true
# What the parser says about parameter lists that are not allowed.
def diagnose(source)
RubyVM::InstructionSequence.compile(source)
"valid"
rescue SyntaxError => e
e.message[/\^~* (.*)$/, 1]
end
sources = [
"def f(a = 1, b, c = 1); end",
"def create_car(make:, model); end",
"def f(&block, other); end",
"def f(a, a); end",
"def f(k: 1, **nil); end",
"def f(...) = g(*)",
"def f(*) = g(...)",
"def setter=(value) = value",
"def add_one value = value + 1",
"def f(@name); end",
"def greet(name) = puts name"
]
diagnostics = sources.to_h { [it, diagnose(it)] }
pp diagnostics
Output:
{"def f(a = 1, b, c = 1); end" => "unexpected parameter order",
"def create_car(make:, model); end" => "unexpected parameter order",
"def f(&block, other); end" => "unexpected parameter order",
"def f(a, a); end" => "duplicated argument name",
"def f(k: 1, **nil); end" =>
"unexpected **nil; no keywords marker disallowed after keywords",
"def f(...) = g(*)" => "unexpected `*`; no anonymous rest parameter",
"def f(*) = g(...)" =>
"unexpected ... when the parent method is not forwarding",
"def setter=(value) = value" =>
"invalid method name; a setter method cannot be defined in an endless method definition",
"def add_one value = value + 1" =>
"expected an `end` to close the `def` statement",
"def f(@name); end" =>
"invalid formal argument; formal argument cannot be an instance variable",
"def greet(name) = puts name" => "valid"}
def create_car(make:, model) in the second line is the classic invalid signature, and the last entry shows that the
command-call body of an endless method (puts name without parentheses) is accepted since Ruby 3.1.
ruby -w adds warnings about the body of a method. They are printed while the file is parsed, so the next file captures them
by compiling four small definitions with $VERBOSE on; the message is the same one ruby -w writes to standard error:
# frozen_string_literal: true
require "stringio"
# Parse-time warnings that `ruby -w` prints about method bodies.
def warnings_for(source)
original = $stderr
$stderr = StringIO.new
$VERBOSE = true
RubyVM::InstructionSequence.compile(source, "demo.rb")
$stderr.string.lines.map(&:chomp)
ensure
$stderr = original
$VERBOSE = false
end
unused = "def total(items)\n sum = 0\n items.size\nend\n"
ambiguous = "p -1\n"
void = "def check(value)\n value == 1\n :ok\nend\n"
unreachable = "def early\n return 1\n 2\nend\n"
warnings_for(unused) # => ["demo.rb:2: warning: assigned but unused variable - sum"]
warnings_for(ambiguous)
# => ["demo.rb:1: warning: ambiguous first argument; put parentheses or a space even after `-` operator"]
warnings_for(void) # => ["demo.rb:2: warning: possibly useless use of == in void context"]
warnings_for(unreachable) # => ["demo.rb:3: warning: statement not reached"]
Pitfalls
-
A mutable default is fine in Ruby.
def collect(book, into = [])builds a new Array on each call; the Python habit of defaulting toNoneand re-creating it is unnecessary. A default that names a constant is shared, however: withDEFAULT_TAGS = []every call gets the same Array, so never mutate it in the method. -
Optional positional parameters plus a Hash.
def f(a, options = {})still works, butf(1, x: 2)andf(1, {x: 2})are now the same call only because no keywords are declared. Add**optsand they are different. Prefer keywords. -
Delegating with
*argsonly. After Ruby 3.0 a trailing keyword Hash stays a positional argument. Use..., explicit*args, **options, &block, orruby2_keywords. -
returninensure. It overrides the method’s result and swallows exceptions. Useensurefor cleanup only. -
block_given?in a block. It reports the block of the enclosing method, not of the inner block. -
Top-level
def. It creates a private method onObjectvisible in every class. Fine in scripts, wrong in a gem. -
Setters and return values.
obj.value = xisxwhatever the setter returns; do not rely on a setter’s return. -
private defand endless methods. Both parse in Ruby 4.0, but older Rubies (3.0 and 3.1) rejectedprivate def name = command argument. -
Reserved words as keywords.
if:andclass:are legal parameter names but not usable as plain local variables; read them withbinding.local_variable_get. -
arityis not a count of declared parameters. Keywords count as one, optional makes it negative, and a proc differs from a lambda. -
method_addedwithoutsuper. Silences every othermethod_addedin the ancestor chain. -
Mismatch between
?and nil. A predicate whose last expression isnilorfalse-ish is fine; RuboCop’sNaming/PredicateMethodandStyle/ReturnNilInPredicateMethodDefinitionencourage a Boolean result for new predicates.
Version notes
| Version | Change that concerns methods and parameters |
|---|---|
2.0 |
Keyword arguments (as a separate syntax); 2.1 made |
2.7 |
|
3.0 |
Positional and keyword arguments separated; endless method definition; |
3.1 |
Anonymous block parameter |
3.2 |
Anonymous |
3.3 |
Error messages name the receiver by class; Prism available as an alternative parser. |
3.4 |
|
4.0 |
|
` lines continue the previous line; |
|
4.1 (preview) |
|
References
-
Method, UnboundMethod and Proc (
parameters,arity,ruby2_keywords) -
Module (
method_added,alias_method,remove_method,ruby2_keywords) and Kernel (__method__,Array,block_given?) -
Ruby 3.0.0 NEWS, 3.1.0, 3.2.0 and 4.0.0
-
The Ruby Style Guide and RuboCop Style cops (
Style/ArgumentsForwarding,Naming/BlockForwarding,Style/OptionalArguments)