Reputation: 109
I'm trying to wrap my head around delegation vs. inheritance so I'm manually delegating a version of Array. One of the specific reasons I read to do this is because when you use things like enumerables, your returned value on the inherited methods reverts back to the parent class (i.e. Array). So I did this:
module PeepData
# A list of Peeps
class Peeps
include Enumerable
def initialize(list = [])
@list = list
end
def [](index)
@list[index]
end
def each(...)
@list.each(...)
end
def reverse
Peeps.new(@list.reverse)
end
def last
@list.last
end
def join(...)
@list.join(...)
end
def from_csv(csv_table)
@list = []
csv_table.each { |row| @list << Peep.new(row.to_h) }
end
def include(field, value)
Peeps.new(select { |row| row[field] == value })
end
def exclude(field, value)
Peeps.new(select { |row| row[field] != value })
end
def count_by_field(field)
result = {}
@list.each do |row|
result[row[field]] = result[row[field]].to_i + 1
end
result
end
protected
attr_reader :list
end
end
When I instantiate this, my include and exclude function great and return a Peeps class but when using an enumerable like select, it returns Array, which prevents me from chaining further Peeps specific methods after the select. This is exactly what I'm trying to avoid with learning about delegation.
p = Peeps.new
p.from_csv(csv_generated_array_of_hashes)
p.select(&:certified?).class
returns Array
If I override select, wrapping it in Peeps.new(), I get a "SystemStackError: stack level too deep". It seems to be recursively burying the list deeper into the list during the select enumeration.
def select(...)
Peeps.new(@list.select(...))
end
Any help and THANKS!
Upvotes: 3
Views: 136
Reputation: 16768
I would recommend using both Forwardable and Enumerable. Use Forwardable to delegate the each
method to your list (to satisfy the Enumerable interface requirement), and also forward any Array methods you might want to include that are not part of the Enumerable module, such as size
. I would also suggest not overriding the behavior of select
as it is supposed to return an array and would at the very least lead to confusion. I would suggest something like the subset
provided below to implement the behavior you are looking for.
require 'forwardable'
class Peeps
include Enumerable
extend Forwardable
def_delegators :@list, :each, :size
def initialize(list = [])
@list = list
end
def subset(&block)
selected = @list.select(&block)
Peeps.new(selected)
end
protected
attr_reader :list
end
Example usage:
peeps = Peeps.new([:a,:b,:c])
subset = peeps.subset {|s| s != :b}
puts subset.class
peeps.each do |peep|
puts peep
end
puts peeps.size
puts subset.size
produces:
Peeps
a
b
c
3
2
Upvotes: 3
Reputation: 11119
As mentioned in the other answer, this isn't really proper usage of Enumerable
. That said, you could still include Enumerable
and use some meta-programming to override the methods that you want to be peep-chainable:
module PeepData
class Peeps
include Enumerable
PEEP_CHAINABLES = [:map, :select]
PEEP_CHAINABLES.each do |method_name|
define_method(method_name) do |&block|
self.class.new(super(&block))
end
end
# solution for select without meta-programming looks like this:
# def select
# Peeps.new(super)
# end
end
end
Just so you know, this really has nothing to do with inheritance vs delegation. If Peeps
extended Array
, you would have the exact same issue, and the exact solution above would still work.
Upvotes: 3
Reputation: 17538
I think that if Peeps#select
will return an Array
, then it is OK to include Enumerable
. But, you want Peeps#select
to return a Peeps
. I don't think you should include Enumerable
. It's misleading to claim to be an Enumerable
if you don't conform to its interface. This is just my opinion. There is no clear consensus on this in the ecosystem. See "Examples from the ecosystem" below.
If we accept that we cannot include Enumerable
, here's the first implementation that comes to my mind.
require 'minitest/autorun'
class Peeps
ARRAY_METHODS = %i[flat_map map reject select]
ELEMENT_METHODS = %i[first include? last]
def initialize(list)
@list = list
end
def inspect
@list.join(', ')
end
def method_missing(mth, *args, &block)
if ARRAY_METHODS.include?(mth)
self.class.new(@list.send(mth, *args, &block))
elsif ELEMENT_METHODS.include?(mth)
@list.send(mth, *args, &block)
else
super
end
end
end
class PeepsTest < Minitest::Test
def test_first
assert_equal('alice', Peeps.new(%w[alice bob charlie]).first)
end
def test_include?
assert Peeps.new(%w[alice bob charlie]).include?('bob')
end
def test_select
peeps = Peeps.new(%w[alice bob charlie]).select { |i| i < 'c' }
assert_instance_of(Peeps, peeps)
assert_equal('alice, bob', peeps.inspect)
end
end
I don't normally use method_missing
, but it seemed convenient.
There doesn't seem to be a consensus on how strictly to follow interfaces.
ActionController::Parameters
used to inherit Hash
. Inheritance ceased in Rails 5.1.ActiveSupport::HashWithIndifferentAccess
still inherits Hash
.Upvotes: 3