I have a class that represents a collection. I included the Enumerable
module into it and defined the method #each
, so that I get all its methods.
But the problem is that Enumerable
's methods don't keep the same class. So, for example, if my class is named Collection
, and if I do Collection#select
, I would like that the result's class is also Collection
(instead of Array
). Is there a way how to achieve this?
Unfortunately, Ruby's Collection Operations are not type-preserving. Every collection operation always returns an Array
.
For collections like Set
s or Tree
s, this is merely annoying, because you need to always convert them back into the type you want to have. But for example for an infinite lazy stream of all prime numbers, this is catastrophic: your program will either hang or run out of memory trying to construct an infinitely large Array
.
Most Collection APIs either eliminate duplicate code or are type-preserving, but not both. E.g. .NET's Collection API mostly eliminates duplicate code, but it always returns the same type: IEnumerable
(equivalent to Ruby's Enumerator
). Smalltalk's Collection API is type-preserving, but it achieves this by duplicating all Collection Operations in every Collection type.
The only Collection API which is type-preserving yet eliminates duplication is Scala's. It achieves this by introducing the new concept of Collection Builders, which know how to efficiently construct a Collection of a specific type. The Collection Operations are implemented in terms of Collection Builders, and only the Collection Builders need to be duplicated … but those are specific to every Collection anyway.
If you want type-preserving Collection Operations in Ruby, you need to either duplicate all Collection Operations in your own Collection (which would be limited to your own code), or redesign the entire Collection API to use Builders (which would require a major redesign of not only your own code but also the existing Collections including every third-party Collection ever written).
It's clear that the second approach is at least impractical if not impossible. The first approach also has its problems, though: Collection Operations are expected to return Array
s, violating that expectation may break other people's code!
You can take an approach similar to Ruby 2.0's lazy collection operations: you could add a new method preserve_type
to your API which returns a proxy object with type-preserving Collection Operations. That way, the departure from the standard API is clearly marked in the code:
c.select … # always returns an Array
c.preserve_type.select … # returns whatever the type of c is
Something like:
class Hash
def preserve_type
TypePreservingHash.new(self)
end
end
class TypePreservingHash
def initialize(original)
@original = original
end
def map(*args, &block)
Hash[@original.map(*args, &block)
# You may want to do something more efficient
end
end