December 25, 2019·6 min read·engineeringrubypatternskeywordslanguage

Ruby 2.7 changes how calls preserve their meaning

Ruby 2.7 ships experimental pattern matching, numbered block parameters, and keyword migration warnings. The release improves expression of intent while exposing older ambiguities in method calls.

A method wrapper can appear to preserve every argument while changing the meaning of an empty hash. That is the kind of compatibility problem Ruby 2.7 makes visible. The December 25 release introduces keyword-conversion warnings on the path to Ruby 3, while also adding experimental pattern matching and shorter forms for common block operations.

These changes affect different parts of a program, but they share a concern about intent. A call needs to distinguish positional data from keyword options. Patterns can state the structure required by a branch. Short collection blocks can express an operation without naming a parameter whose role is already obvious. Each choice changes how much meaning a reader must reconstruct from surrounding code.

I like Ruby for its readable object-oriented code and its blocks. The useful measure for these additions is whether they preserve that readability as a program grows. Shorter syntax helps when it removes repetition, while explicit syntax helps when the previous form leaves an important distinction ambiguous.

A method call has more structure than a final hash

Ruby 2.7 deprecates automatic conversion between positional and keyword arguments in cases that are planned to change in Ruby 3. The immediate application effect is often a warning while the old conversion still produces a result. That permits migration work to begin before the future calling convention rejects the call.

The detailed December 12 keyword-separation explanation distinguishes a hash supplied as one value from a hash expanded into keywords. A positional data record and an option list can contain the same keys while serving different purposes. The double splat makes keyword intent explicit, while braces can preserve a positional hash.

That is a property of the interface, not merely of key names. A method expecting one data object should receive that object intact. A method declaring keyword parameters should receive the corresponding keyword information. Repairing a deprecated conversion therefore requires understanding the intended call, rather than applying the same textual edit to every hash argument.

An existing options-hash API is not automatically obsolete. The explanation explicitly preserves keyword-like calls to methods that accept an options hash and no keyword parameters. That case has a defined interpretation without the same ambiguity. The migration targets automatic conversions whose meaning depends on the relationship between positional and keyword parameters.

Delegation has to preserve argument categories

A wrapper that receives *args and a block has historically relied on conversion to carry some keyword calls through to its target. Once positional and keyword arguments are distinct, forwarding only positional arguments does not express the entire calling contract. A wrapper needs to preserve the categories it received.

Ruby 2.7 adds a concise forwarding form for methods that do not alter the arguments. The release NEWS file states that it forwards positional, keyword, and block arguments.

def dispatch(...)
  target(...)
end

This syntax is useful when the wrapper's job is delegation. It avoids independently unpacking and reassembling categories whose distinction the wrapper does not need to inspect. The parentheses are part of the syntax, and the form requires the new interpreter rather than offering compatibility with earlier Ruby parsers.

Libraries supporting older Rubies have a different constraint. The keyword-separation article describes ruby2_keywords as a migration mechanism for traditional argument forwarding. That mechanism retains information needed to treat the final argument as keywords when it reaches the target. It is a compatibility tool for delegation, not a general instruction to make every positional hash behave as keyword options.

The article also documents an empty-hash corner case in explicit *args, **kwargs forwarding on Ruby 2.7. An empty positional hash can be absorbed into keyword rest and then disappear when forwarded. That example explains why a superficially more explicit wrapper is not automatically portable across every interpreter involved in the transition.

For an application wrapper, this makes a useful test requirement. A no-argument call and a call containing one empty positional hash need separate cases if the target distinguishes them. The wrapper should preserve the difference. A test containing only a nonempty options hash can miss the ambiguity because the nonempty contents make the intended interpretation easier to recover.

Patterns combine a structural condition with extraction

Pattern matching remains experimental in the final 2.7 release. It gives case an in form that can inspect nested arrays and hashes while binding matched values. The NEWS file demonstrates both extraction and failure when no pattern matches.

This example follows that released array syntax.

value = [0, [1, 2, 3]]

case value
in [first, [second, *remaining]]
  p [first, second, remaining]
else
  p :unexpected
end

The pattern describes an outer pair whose second element has its own structure. It extracts the first two values of interest and binds the remaining nested elements. The body does not need to repeat the entire navigation path after separately establishing that the shape is suitable.

The design history in Feature 14912 explores how this structural work fits Ruby's object model. Discussion of decomposition and class identity shows why the feature needs a Ruby-specific contract. Matching an object's contents and establishing which kind of object it is are related requirements, but they are not interchangeable.

A pattern also does not replace every validation rule. Its branch can extract an integer-shaped piece of data while still needing a domain check on its value. Successful structural matching says the pattern's requirements were satisfied. It does not establish that the record is appropriate for every later operation.

I find patterns most useful where they keep the condition and the extraction together. An elaborate pattern that encodes too many unrelated business rules can be as difficult to read as the conditionals it replaces. Naming a separate operation remains useful when the rule has meaning beyond the immediate data shape.

Short blocks can keep their operation visible

Ruby 2.7 introduces numbered default block parameters. The NEWS examples use _1 for a single argument and show multiple numbered parameters with collection elements. A simple conversion can therefore be written without introducing an explicit local parameter name.

[1, 2, 10].map { _1.to_s(16) }

This is readable because the body has one operation and one obvious input. As a block grows, a descriptive parameter name can carry information that a number does not. The new syntax provides a choice for concise expressions rather than establishing that every block should discard named parameters.

The release also adds Enumerable#tally, which directly counts occurrences of each element. This is an example of a library operation removing more repetition than a syntax abbreviation alone.

%w(red blue red).tally

The result maps each distinct element to its count. An application no longer needs to express the accumulator setup and repeated increment merely to communicate that operation. A reader can identify the intended aggregation from the method name, while a custom accumulation remains appropriate for a different requirement.

Compaction addresses memory organization

GC.compact is another addition in the release. The announcement and compaction proposal describe moving live heap objects so that fewer pages may be needed and the heap may become more copy-on-write friendly. This concerns the placement of surviving objects, which is separate from deciding which objects are no longer live.

A heap can contain enough total free space while its live objects remain scattered across pages. Reclaiming dead objects and organizing the survivors address different aspects of that state. Compaction supplies the latter operation explicitly, so its usefulness depends on the organization of the particular workload's heap.

That distinction limits performance claims. The existence of the method does not establish a fixed memory reduction for an arbitrary Ruby application. A process whose surviving objects already occupy a compact set of pages presents a different opportunity from one with substantial fragmentation. The API makes the operation available without making every workload equivalent.

The release distinguishes exploration from migration

The final announcement keeps experimental status visible for pattern matching, while the keyword warnings identify a defined compatibility transition. Those are different reasons for an application to examine new interpreter behavior. A developer can explore a new pattern without assuming the entire feature contract is permanent, and can repair a warned call whose conversion is scheduled for removal.

The release supplies -W:no-deprecated and Warning[:deprecated] = false for controlling deprecation-warning output. Suppressing that output changes presentation. It does not rewrite the call or preserve a conversion after its planned removal. The substantive migration remains the choice between positional data and keyword arguments at the method boundary.

Library authors therefore need to consider the interpreters their consumers still run. Forwarding syntax available in 2.7 cannot be parsed by older releases merely because it expresses the desired behavior clearly. A compatibility mechanism and a new-language implementation can solve related problems under different support requirements. The relevant choice follows the versions the library promises to accept, along with the argument categories its public methods need to preserve.