Endless Methods
Ruby’s endless method syntax lets you define a method in a single line by writing its entire body as one expression after an equals sign — almost like writing a mathematical function definition. Introduced in Ruby 3.0 and refined in Ruby 3.1, it’s a natural fit for short, single-purpose methods: predicates, conversions, simple calculations, and delegators. Endless methods don’t add new capabilities to Ruby; they’re simply a more concise way to write a method whose only job is to compute and return one value.
Overview: How Endless Methods Work
Every Ruby method, endless or not, works the same way under the hood: Ruby evaluates the body and implicitly returns the value of the last expression it evaluated. A regular method makes this explicit across multiple lines, with the final expression before end becoming the return value. An endless method collapses that same behavior into its most literal form: there’s no “last line” to find, because there’s only one line, and its value is the return value. That’s also why the feature is called “endless” — there’s no matching end keyword, because the body is a single expression rather than a block of statements that needs one.
Because endless methods are ordinary methods in every other respect, everything you already know about methods still applies. They accept positional, keyword, splat, and block parameters. They respect Ruby’s visibility modifiers (private, protected, public). They can be instance methods or, written with a self. prefix, class (module) methods. And calling one still goes through Ruby’s normal method lookup: Ruby searches the receiver’s singleton class, then its class, then any included modules (in reverse inclusion order), then the superclass, and so on up the ancestor chain until it finds a match or reaches BasicObject. Endless syntax only changes how the method is written, not how it behaves once defined.
The one real constraint is that the body must be exactly one expression — not necessarily one line of code, but one expression. A ternary (condition ? a : b), a boolean short-circuit (a || b), a chain of method calls, or even a multi-line if (since if is itself an expression in Ruby that evaluates to a value) can all be valid endless bodies. A sequence of independent statements cannot.
Ruby’s parser also needs a way to tell an endless method definition apart from the start of a regular one, which is where a subtle but important rule comes in: if the method takes any parameters, they must be wrapped in parentheses. Without parentheses, def square x = x * x is ambiguous — Ruby reads x = x * x as a default value being assigned to the parameter x (a legal, if self-referential, default), and then keeps expecting the rest of a regular multi-line method body, which never arrives. Wrapping the parameter in parentheses, def square(x) = x * x, removes the ambiguity completely. If a method takes no parameters at all, the parentheses are optional: def pi = 3.14159 and def pi() = 3.14159 are both valid and equivalent.
Syntax
def method_name(param1, param2) = expression
def— starts the method definition, exactly as with a regular method.method_name— the method’s name. Like any Ruby method, it can end in?for a predicate (even?) or, less commonly for endless methods,!for a mutating variant.(param1, param2)— the parameter list. Parentheses are required if there is at least one parameter; they’re optional if there are none.=— separates the method signature from its body. This is what tells Ruby “this is an endless method.”expression— a single expression whose value becomes the method’s return value, evaluated exactly like the last line of a normal method body.
To define a class or module method instead of an instance method, prefix the name with self., just as you would for a regular method: def self.method_name(param1) = expression.
Examples
Example 1: A simple calculation
def square(x) = x * x
puts square(5)
puts square(-3)
Output:
25
9
The body, x * x, is the entire method — there’s no end, and no explicit return. Calling square(5) binds x to 5, evaluates 5 * 5, and that value, 25, is returned and printed. square(-3) works the same way: -3 * -3 evaluates to 9.
Example 2: A predicate method
def adult?(age) = age >= 18
puts adult?(20)
puts adult?(15)
puts adult?(18)
Output:
true
false
true
Endless methods are a great fit for predicate methods, whose entire purpose is to evaluate one boolean expression and return it. age >= 18 already evaluates to true or false on its own, so wrapping it in a multi-line def...end would only add ceremony without adding clarity.
Example 3: A class method
class Temperature
def self.celsius_to_fahrenheit(c) = (c * 9.0 / 5) + 32
end
puts Temperature.celsius_to_fahrenheit(0)
puts Temperature.celsius_to_fahrenheit(100)
puts Temperature.celsius_to_fahrenheit(37)
Output:
32.0
212.0
98.6
The self. prefix makes this a class method, callable as Temperature.celsius_to_fahrenheit(...) without ever instantiating a Temperature object. Using 9.0 (a float) instead of 9 forces floating-point division, so 0 * 9.0 / 5 + 32 is 32.0, not the integer 32. This is a common pattern: small conversion or lookup methods on a class are often perfect candidates for endless syntax.
Example 4: Keyword arguments and a conditional expression
def greeting(name:, formal: false) = formal ? "Good day, #{name}." : "Hey #{name}!"
puts greeting(name: "Ava")
puts greeting(name: "Mr. Smith", formal: true)
Output:
Hey Ava!
Good day, Mr. Smith.
This example shows that endless methods support keyword arguments and default values exactly like regular methods — only the body is restricted, not the signature. The ternary operator (condition ? then_value : else_value) is itself a single expression, so it fits perfectly as an endless method body even though it branches on a condition.
How It Works Step by Step
Tracing square(5) from Example 1 shows exactly what Ruby does when it encounters an endless method call:
- Ruby looks up the method named
squareon the receiver (here, the top-levelmainobject), following the normal ancestor-chain lookup. - It finds the definition
def square(x) = x * xand binds the argument5to the parameterx. - It evaluates the single expression that makes up the body,
x * x, which becomes5 * 5. - The result,
25, becomes the method’s return value. There’s no explicitreturn, and none is needed — implicit return simply means “the value of the last expression evaluated,” and here there’s only one expression to evaluate. - Control returns to the caller,
puts square(5), which prints the returned value.
This is identical to how a regular method returns a value. Endless syntax doesn’t introduce new return semantics — it just makes the single-expression case visually compact by removing the parts (end, an explicit final line) that would otherwise be redundant.
Common Mistakes
Mistake 1: Forgetting parentheses around parameters
def square x = x * x
puts square(5)
This looks reasonable, but it doesn’t compile. Without parentheses, Ruby can’t tell whether x = x * x is a default value for parameter x of a regular method, or the body of an endless method. Ruby resolves the ambiguity by always treating it as a default parameter value, so it then expects a full def...end body to follow — and since end never arrives, this raises a SyntaxError. The fix is simply to always parenthesize the parameter list:
def square(x) = x * x
puts square(5)
Output:
25
Mistake 2: Trying to fit multiple statements into the body
def log_and_square(x) = puts("squaring #{x}"); x * x
puts log_and_square(5)
It’s tempting to think the semicolon joins two statements into one method body, the way it might inside a do...end block. It doesn’t. The endless method ends at the first expression, puts("squaring #{x}"), so log_and_square actually returns whatever puts returns — always nil. Everything after the semicolon, x * x, becomes a separate top-level statement that Ruby tries to run immediately, and since x isn’t defined outside the method, this raises NameError: undefined local variable or method 'x'. If you need both a side effect and a return value from one endless method, reach for something like Object#tap, which runs a block for its side effect and then returns the original receiver, keeping the whole thing a single expression:
def log_and_square(x) = x.tap { puts "squaring #{x}" } * x
puts log_and_square(5)
Output:
squaring 5
25
Best Practices
- Reach for endless methods when the whole job of the method is “compute one value and return it” — predicates like
even?, conversions, simple math, and delegators are ideal candidates. - Always wrap parameters in parentheses, even for a single parameter (
def double(x) = x * 2), both to avoid the default-value parsing ambiguity and to match the style the Ruby community has converged on. - Keep the body to one clear expression. If you find yourself needing multiple branches of logic, several statements, or intermediate local variables to stay readable, switch to a regular
def...endmethod instead — endless syntax exists for conciseness, not for cramming. - Remember that
defitself evaluates to a symbol naming the method it defines, so you can combine an endless method with a visibility modifier on one line:private def helper(x) = x * 2. - Ternaries and
||/&&are fine for simple conditional bodies, but stop reaching for them once the logic needs more than one branch — nested ternaries hurt readability fast, endless or not. - Don’t avoid endless methods just because a method’s signature is rich — keyword arguments, defaults, and splats all work normally. Only avoid endless syntax when the body would need more than one expression.
Practice Exercises
- Write an endless method
cube(x)that returnsxraised to the third power. Callingcube(3)and printing the result should show27. - Write an endless predicate method
even_number?(n)that returnstrueifnis even andfalseotherwise. Hint: use the modulo operator%and compare the remainder to0. - Write a class
Circlewith a class methodself.area(radius), defined as an endless method, that returns the circle’s area usingMath::PI. CallingCircle.area(2)should print approximately12.566370614359172.
Summary
- Endless methods use the form
def name(params) = expression, introduced in Ruby 3.0 and refined in Ruby 3.1. - The body must be exactly one expression; its value is implicitly returned, exactly like any other Ruby method — there is no separate return mechanism to learn.
- Parentheses around parameters are required whenever a method takes at least one parameter, and optional when it takes none.
- Endless methods are ordinary methods in every other respect: keyword arguments, defaults, splats, visibility modifiers, and
self.class methods all work exactly as they do withdef...end. - Reserve endless syntax for short, single-purpose methods, and switch back to a regular
def...endmethod once the logic needs more than one expression to stay readable.
