Feature #17551
closedPattern Matching - Default Object#deconstruct and Object#deconstruct_keys
Description
Pattern Matching is a very powerful feature, but many classes are not able to enjoy its functionality due to the lacking of a default deconstruct
and deconstruct_keys
method being present.
This feature request is to introduce a series of sane defaults, and what they might look like. What these defaults are or should be is up for debate, but I would like to propose a few ideas to get things started.
Reasonable Defaults¶
The Fast Version¶
I will elaborate on this in the below content, but the fast version of my proposal is:
-
deconstruct_keys
should default to a classes public API -
deconstruct
should default to being an alias ofto_a
or otherArray
coercion methods
Deconstruct Keys¶
deconstruct_keys
is used for extracting values out of an object in use with a Hash
-like pattern match. In the case of a literal Hash
with Symbol
keys the deconstructed keys are extracted from the Hash
.
My proposal would be to base the default deconstruct_keys
on the attributes of an object as defined by attr_*
methods. Consider this Person
class:
class Person
attr_reader :name, :age, :children
def initialize(name:, age:, children: [])
@name = name
@age = age
@children = children
end
end
The attributes exposed by the proposed default deconstruct_keys
would be name
, age
, and children
.
As attr_reader
has made these values public they are the interface into the class, meaning this will not break encapsulation of values and relies on the already established API it provides.
In current Ruby this behavior can be approximated as seen here in a test gem I call Dio: https://github.com/baweaver/dio#attribute-forwarder
It does a comparison of instance variables versus all methods to find public readers:
ivars = Set.new base_object
.instance_variables
.map { _1.to_s.delete('@').to_sym }
all_methods = Set.new base_object.methods
attributes = ivars.intersection(all_methods)
Which allows me to do this:
Person.new(
name: 'Alice',
age: 40,
children: [
Person.new(name: 'Jim', age: 10),
Person.new(name: 'Jill', age: 10)
]
)
case Dio.attribute(alice)
in { name: /^A/, age: 30..50 }
true
else
false
end
case Dio.attribute(alice)
in { children: [*, { name: /^J/ }, *] }
true
else
false
end
My list of ideas for this default deconstruct_keys
method are:
-
attr_
based - Any exposed attribute - public method based (
public_send
) - All public methods on the class - all methods (
send
) - Every potential method
I believe the first is the most conservative and Ruby-like, as well as the least surprising. A case could be made for the second which allows for more flexibility and remains within the encapsulation of the class. The third is more unrealistic as it exposes everything.
I would like to discuss between the first two.
Deconstruct¶
deconstruct
is used for extracting values out of an object in use with an Array
-like pattern match. In the case of an Array
the values are returned directly.
My proposal would be to base the default deconstruct
on the Ruby concept of Duck typing through to_a
or Enumerable
:
module Enumerable
alias_method :deconstruct, :to_a
end
Consider this Node
class:
class Node
attr_reader :value, :children
def initialize(value, *children)
@value = value
@children = children
end
def to_a() = [@value, @children]
def self.[](...) = new(...)
end
It is Array
-like in nature, and through to_a
we could infer deconstruct
instead of explicitly requiring a method:
tree = Node[1,
Node[2, Node[3, Node[4]]],
Node[5],
Node[6, Node[7], Node[8]]
]
case tree
in [1, [*, [5, _], *]]
true
else
false
end
I believe this is a good use of duck typing, and presents a reasonable default. If no Array
coercion methods are available it would make sense that it cannot be pattern matched against like an Array.
My proposal here is to use the established to_a
or other Array
coercion methods to imply deconstruct
Why Defaults?¶
Many Ruby gems and code do not implement deconstruct
or deconstruct_keys
, meaning pattern matching cannot be used against them easily. This change will allow for pattern matching against Ruby code from any generation, and open up the feature to far more use across code bases.
I believe this feature would not be substantial work to implement, but will have substantial gains for all Ruby code.
Thank you for your time in reading, and I apologize for another long feature request.