Ruby variables do not keep the same class

I have a class representing a collection. I included the Enumerable module in it and defined the #each method to get all of its methods.

But the problem is that Enumerable methods do not support the same class. So, for example, if my class is named Collection , and if I am doing Collection#select , I would like the result class to also be Collection (instead of Array ). Is there any way to achieve this?

+4
source share
3 answers

Unfortunately, Ruby Collection operations are not stored in the type. Each collection operation always returns an Array .

For collections like Set or Tree s, this is just annoying because you need to always convert them back to the type you want to have. But, for example, for an endless lazy stream of all primes this is catastrophic: your program either freezes or runs out of memory, trying to build an infinitely large Array .

Most Collection APIs either eliminate duplicate code or retain the type, but not both. For instance. The API.NET Collection basically eliminates duplicate code, but it always returns the same type: IEnumerable (equivalent to Ruby Enumerator ). The Smalltalk Collection API is saved in the type, but it does this by duplicating all collection operations in each type of collection.

The only Collection API that preserves the type but eliminates duplication is Scala. He accomplishes this by introducing a new concept for collectors who can efficiently create a collection of a certain type. Operations with collections are implemented from the point of view of collectors, and only collectors of collections need to be duplicated ... but they are still specific to each collection.

If you want collection operations with type preservation to be stored in Ruby, you need to either duplicate all collection operations in your own collection (which will be limited by your own code), or redesign the entire Collection API to use Builders (which will require a basic redesign not only of yours own code, but also existing Collections, including every third-party collection ever written).

Clearly, the second approach is at least impractical, if not impossible. The first approach also has its problems, though: it is expected that operations with collections will return Array s, violating this expectation, can break other people's code!

You can use an approach similar to Ruby 2.0 lazy collection: you can add a new preserve_type method to your API that returns a proxy object containing Collection Operations. Thus, the departure from the standard API is clearly indicated in the code:

 c.select … # always returns an Array c.preserve_type.select … # returns whatever the type of c is 

Sort of:

 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 
+3
source

Since Enumerable#select is for returning an array, you need to specify somewhere how to map this to the Collection instance. This means that you explicitly need to define Collection#select . Otherwise, Ruby will not know the matching rule from the original result of the Enumerable#select array to the Collection instance.

+5
source

Another way could be to create a proxy server for the base array:

 class Collection def initialize( items= nil ) @items = items || [] end def respond_to_missing?(method_name, include_private = false) Enumerable.instance_methods.include? method_name end def method_missing name, *args, &block if @items.respond_to? name res = @items.send name, *args, &block res.kind_of?( Array ) ? Collection.new(res) : res else super end end end 

in IRB:

 col = Collection.new [1,2,3] => #<Collection:0x0000010102d5d0 @items=[1, 2, 3]> col.respond_to? :map => true col.map{|x| x * 2 } => #<Collection:0x000001009bff18 @items=[2, 4, 6]> 
+2
source

Source: https://habr.com/ru/post/1442422/


All Articles