skip to content

In Ruby 4.0, what does `def fahrenheit = celsius * 9 / 5.0 + 32` define, and what does that def expression itself return?

level: middleimportance: should knowfreq 45%

answer

  1. one expression after =
  2. parentheses required for parameters
  3. no setters, SyntaxError
  4. def evaluates to a Symbol
  5. private def works on one line

basics

~20 s

It defines an instance method fahrenheit whose body is that one expression; endless defs arrived in Ruby 3.0. The def expression itself evaluates to the Symbol :fahrenheit, which is why private def fahrenheit = ... works.

solid answer

~40 s

It is an **endless method definition**, added in Ruby 3.0: `def name(params) = expression` defines a method whose body is that single expression, equivalent to a `def ... end` with one line. Parameters need parentheses, setters (`name=`) cannot be written this way, and since 3.1 the body may be a command call such as `def greet = puts "hi"`. Separately, every `def` is itself an expression that evaluates to the method's name as a **Symbol**, so this line returns `:fahrenheit`. That is what lets methods taking a name wrap a definition: `private def fahrenheit = ...` defines the method and passes `:fahrenheit` to `private` in one line. One trap: a trailing `if` modifier guards the whole definition, not the body.

code

ruby · 14 lines
ruby
class Temperature
  attr_reader :celsius

  def initialize(celsius) = @celsius = celsius

  def fahrenheit = celsius * 9 / 5.0 + 32
  def self.parse(text) = new(Float(text)) rescue nil

  private def kelvin = celsius + 273.15
end

Temperature.new(30).fahrenheit      # => 86.0
Temperature.parse("abc")            # => nil
Temperature.new(30).respond_to?(:kelvin)  # => false

go deeper

for a junior

Recall the shape def name(args) = expression and that it returns that expression, like a one-line def ... end.

for a middle

Explain the restrictions (parentheses, no setters), the 3.1 command-call body, and why def returning :name lets private def work on one line.

for a senior

Spot the trailing-if trap that silently skips a definition, and set Style/EndlessMethod and Style/AmbiguousEndlessMethodDefinition so the team uses the form only where it reads clearly.

for a principal

Decide whether endless defs belong in the codebase's style at all, weighing terseness for small derived values against churn when bodies grow and mixed styles in review.

## The endless method definition Ruby 3.0 added a shorthand for methods whose body is **exactly one expression**: ```ruby class Temperature attr_reader :celsius def initialize(celsius) = @celsius = celsius def fahrenheit = celsius * 9 / 5.0 + 32 def freezing? = celsius <= 0 def scaled(factor) = celsius * factor end Temperature.new(30).fahrenheit # => 86.0 ``` The form is `def name = expression` or `def name(params) = expression`. There is no `end`, and the method returns the value of that expression exactly as a one-line `def ... end` would. Ruby's syntax documentation calls it the shorthand syntax, and its 3.0 release notes introduced it as an experimental feature. ## Rules and restrictions | Rule | Example | Result | |---|---|---| | Parameters need parentheses | `def add_one value = value + 1` | `SyntaxError` | | Setters are not allowed | `def celsius=(v) = @celsius = v` | `SyntaxError` | | Command call as the body (3.1+) | `def greet = puts "hi"` | valid | | `rescue` modifier | `def parse(s) = Float(s) rescue nil` | rescue belongs to the body | | `if` modifier | `def cold? = true if winter` | guards the **definition** | A few points behind the table: - The setter restriction has its own error message: a setter method cannot be defined in an endless method definition. - Ruby 3.1 allowed command calls without parentheses in the body. Its release notes said `private def foo = puts "Hello"` did not parse; Ruby 4.0's own test suite asserts that it does. - The two modifiers bind differently. The `rescue` modifier becomes part of the method body. A trailing `if`, `unless`, `while` or `until` applies to the whole `def`, so when the condition is false the method is never defined. RuboCop's pending cop `Style/AmbiguousEndlessMethodDefinition` flags this; write `def cold? = (true if winter)` if the condition belongs in the body. - RuboCop's pending `Style/EndlessMethod` cop defaults to `allow_single_line`, which rejects endless definitions spread across several lines. ## def is an expression that returns a Symbol Since Ruby 2.1, a `def` is not just a statement: it **evaluates to the method's name as a Symbol**. This is true for regular and endless forms and for singleton definitions such as `def self.from_fahrenheit(f)`. ```ruby (def fahrenheit = 86.0) # => :fahrenheit (def self.parse(s); end) # => :parse ``` That return value exists so that methods which accept a method name can wrap a definition directly: 1. **Visibility on one line.** `private def fahrenheit = ...` defines the method, then calls `private(:fahrenheit)`. What `private` does to the method is a visibility question; the part that belongs here is that `def` supplies the Symbol it needs. 2. **Other name-taking methods.** `module_function def helper ... end` works the same way, as does any class-level macro of your own that accepts a Symbol, for example one that wraps a method with caching. ## Common traps in interviews and reviews 1. **Believing `def` returns `nil`.** That was true before Ruby 2.1; since then it returns the Symbol, which is what `private def` relies on. 2. **Expecting a trailing `if` to guard the body.** It guards the definition; parenthesise the body to keep the condition inside. 3. **Writing a writer in endless form.** The parser rejects it, so writers stay as `def celsius=(value) ... end` or come from `attr_writer`. 4. **Treating endless methods as a different kind of method.** They are ordinary methods: the same visibility rules, the same parameters and arity, the same Symbol from the `def` expression. Only the source syntax differs. ## When to use the endless form - **Good fits:** small derived values (`fahrenheit`), predicates (`freezing?`), simple delegations and conversions such as `def to_s = "#{celsius} C"`. - **Poor fits:** anything with branching, side effects in sequence, or a body you would want to extend later; converting back to `def ... end` is churn in the diff. - **Never:** writers, which the parser rejects, and conditional definitions written with a trailing `if` by accident. ## Key points - `def name(params) = expr` is a one-expression method, available since Ruby 3.0. - Parentheses are mandatory around parameters; setters are a `SyntaxError`. - A trailing `rescue` modifier is part of the body; a trailing `if` wraps the definition. - Every `def` evaluates to a Symbol, which lets `private def ...` and similar wrappers work on one line.

  • In Ruby 4.0, what does `def cold? = true if ENV["WINTER"]` do when WINTER is not set?
    It defines nothing. The trailing `if` modifier applies to the whole `def` expression, so the definition runs only when the condition is truthy and `cold?` is then missing. To keep the condition in the body, write `def cold? = (true if ENV["WINTER"])`.
  • Why does `private def fahrenheit = celsius * 9 / 5.0 + 32` work as a single line in Ruby?
    The `def` runs first and evaluates to the Symbol `:fahrenheit`, which becomes the argument to `private`. So one line both defines the method and names it for `private`. Any method that takes a method name, such as `module_function`, can wrap a `def` the same way.

saying these in an interview costs you the question

  • A def expression returns nil, so it cannot be passed to another method.
  • An endless def can define a setter such as celsius=.
  • Parameters on an endless def work without parentheses.
  • def cold? = true if winter defines cold? returning nil when winter is false.
  • Endless defs return a Method object instead of a Symbol.