Ruby / Foundations: values, methods, text, and valid objects / Lesson 3
Design method, keyword, and block contracts
Use explicit parameter roles, implicit returns, keyword forwarding, yield, and mutation conventions without hidden assumptions.
On this page
Lesson goals
- Distinguish positional arguments, keyword arguments, and a passed block in Ruby 3.3.
- Explain return and side-effect behavior as part of a callable’s contract.
Make argument roles visible at the call site
Ruby methods can accept required or optional positional arguments, keyword arguments, rest arguments, and a block. A keyword such as id: can be required while copies: 1 provides a default. Ruby 3 separates keyword arguments from an ordinary final positional hash in important calling and forwarding cases. Use ** when deliberately forwarding keyword data rather than relying on older implicit conversions. Avoid broad rest parameters that silently accept misspelled options. A clear signature documents what the caller must provide, but dynamic types still require semantic validation inside the appropriate boundary.
Use names without hiding the return value
The label method returns its final interpolation expression. puts is outside the reusable method so callers can use the value without console output. A default can be an expression evaluated when that argument is omitted; it is not necessarily one preallocated object shared forever. Nevertheless, returning or storing a supplied mutable object can share it with the caller. Document whether the method borrows, copies, or mutates arguments. The example uses fixed strings and integers only and provides expected output, since no Ruby interpreter was available for execution.
def record_label(id:, copies: 1)
"#{id}:#{copies}"
end
options = {id: 'A-7', copies: 2}
puts record_label(id: 'A-6')
puts record_label(**options)
A-6:1
A-7:2
Expected output is A-6:1 then A-7:2. The options hash is expanded as keywords. The script is self-contained and unexecuted.
A block supplies behavior under the method’s protocol
A method can invoke the supplied block with yield, and block_given? lets it distinguish a missing block. Decide whether the block is mandatory, optional, invoked once, or invoked for every element. Those choices affect effects and exceptions. Calling yield without a supplied block can fail, so a method with optional behavior needs a declared alternative. A block is not automatically a background task; yield calls it within the current control flow. Later lessons distinguish blocks, Proc objects, and lambdas because their argument and return behavior can differ in consequential ways.
Keep the yielded value and method result intentional
The helper below computes a normalized local label, then either returns it or returns the block’s transformation. It does not mutate the input string. The block receives one argument and its final expression becomes the helper result in that branch. Adding a diagnostic puts as the last expression would change that result to nil. Such behavior is useful when it is the declared protocol and surprising when the method promises to always return the original value. Keep transformation helpers separate from observers that preserve the receiver regardless of what a callback returns.
def with_label(text)
value = text.strip.upcase
return value unless block_given?
yield value
end
puts with_label(' archive ')
puts with_label(' archive ') { |label| "[#{label}]" }
ARCHIVE
[ARCHIVE]
Expected output is ARCHIVE then [ARCHIVE]. The block runs synchronously and determines the second result. No runtime test is claimed.
Names communicate conventions, not enforcement
A ? suffix conventionally marks a predicate, and ! often warns of a more dangerous or mutating alternative. It does not universally mean the method mutates, nor does a non-bang name guarantee purity. A setter-style method name also has calling semantics that should be read carefully rather than inferred from ordinary function syntax. Define invalid-input behavior, mutation, and return values explicitly. Prefer a small method whose responsibility can be named without a long option list; overly flexible signatures can move errors into obscure downstream branches instead of preventing them.
Common errors and fixes
A forwarded hash is not treated as the intended keywords.
- Why it happens
- The caller relied on implicit positional-to-keyword conversion.
- How to fix it
- Use explicit ** forwarding with the correct keyword contract.
- Prevent it next time
- Check wrappers against Ruby 3 keyword separation rules.
A helper returns nil after adding a log line.
- Why it happens
- The final expression became puts instead of the intended value.
- How to fix it
- Return the intended value explicitly or keep it as the final expression.
- Prevent it next time
- Review return contracts during instrumentation changes.
yield fails when no block was supplied.
- Why it happens
- The implementation assumed a mandatory block without documenting or checking it.
- How to fix it
- Require the block clearly or provide an explicit block_given? alternative.
- Prevent it next time
- Test supplied-block and missing-block paths independently.
Check your understanding
1. Does a method ending in ! always modify its receiver?
Show worked solution
No. It is a naming convention for a dangerous or surprising variant and must be interpreted through the actual API documentation. Some methods mutate without !, and a bang variant may differ by failure behavior. Inspect the contract and test relevant state changes rather than deriving ownership or safety from the suffix alone.
2. Why keep puts outside record_label?
Show worked solution
It lets the method return reusable data while the caller owns presentation. Another caller can store, compare, or transform the label without capturing console output. This separation does not validate the identifier or make input objects immutable, so retain those checks at their own boundaries.
Key takeaways
- Argument roles, block invocation, mutation, and return value are one method contract.
- Ruby conventions help readers but do not replace explicit behavior guarantees.