Project

General

Profile

Actions

Feature #17551

closed

Pattern Matching - Default Object#deconstruct and Object#deconstruct_keys

Added by baweaver (Brandon Weaver) almost 4 years ago. Updated almost 4 years ago.

Status:
Closed
Assignee:
-
Target version:
-
[ruby-core:102127]

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:

  1. deconstruct_keys should default to a classes public API
  2. deconstruct should default to being an alias of to_a or other Array 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:

  1. attr_ based - Any exposed attribute
  2. public method based (public_send) - All public methods on the class
  3. 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.

Updated by marcandre (Marc-Andre Lafortune) almost 4 years ago

Your example would have deconstruct* methods had it subclassed Struct.new(:name, :age, :children) (even though there are many things I personally dislike about Struct)

I don't think there is a sensible default for either methods, other than maybe use to_ary and to_hash if those existed. I believe the builtin classes that define these also define deconstruct*.

More importantly, even if there was a good default, it is too late to change it as the incompatibility could be enormous. Hash defines to_a but does not define deconstruct (for good reasons imo), but your proposal would. Code that does not match in 3.0 might suddenly match in 3.1, or worse cause some strange side effects (imagine trying to match with clear: or freeze: keys...)

I think it is best to leave no default and we don't have a choice anyways. Instead, let's open issues with those gems that would benefit from defining deconstruct*...

Updated by baweaver (Brandon Weaver) almost 4 years ago

That is a very fair point, and I appreciate your insight there. I would be tempted to add it to Enumerable except in that Hash also implements that same interface.

Updated by marcandre (Marc-Andre Lafortune) almost 4 years ago

Interestingly, the gem I immediately thought of (ast) needs a deconstruct that is different from to_a...

Updated by Eregon (Benoit Daloze) almost 4 years ago

One issue if we use the calls to attr_* is we would consider private attr_* like private attr_reader :foo, which seems wrong.
And of course at any time the visibility of methods can change, so it would probably be quite expensive to call deconstruct_keys, e.g., every call would need to check the methods are still public.

Updated by baweaver (Brandon Weaver) almost 4 years ago

I believe this issue should be closed, as it has an alternative resolution available:

I will start to implement pattern matching interfaces in Ruby core classes as well as common gems, and I will link back providing tracking information on this project.

Actions #6

Updated by marcandre (Marc-Andre Lafortune) almost 4 years ago

  • Status changed from Open to Closed
Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0Like0Like0