Forum: Ruby Enumerable ObjectSpace (#222)

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
33117162fff8a9cf50544a604f60c045?d=identicon&s=25 Daniel X Moore (yahivin)
on 2009-10-24 19:44
(Received via mailing list)
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

The three rules of Ruby Quiz:

1.  Please do not post any solutions or spoiler discussion for this
quiz until 48 hours have elapsed from the time this message was
sent.

2.  Support Ruby Quiz by submitting ideas and responses
as often as you can.

3.  Enjoy!

Suggestion:  A [QUIZ] in the subject of emails about the problem
helps everyone on Ruby Talk follow the discussion.  Please reply to
the original quiz message, if you can.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

RSS Feed: http://rubyquiz.strd6.com/quizzes.rss

Suggestions?: http://rubyquiz.strd6.com/suggestions

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

## Enumerable ObjectSpace (#222)

Kaixo Rubyists,

This week's quiz was suggested by Trans[1].

ObjectSpace has the method #each_object. I've always wanted to alias
that to #each and make ObjectSpace Enumerable. But there's a catch,
#each_object takes an argument and Enumerable does not pass arguments
on to the #each method. So how does one resolve this issue? What's the
easiest way to make ObjectSpace Enumerable?

One could always take the brute force approach and completely
re-implement Enumerable module to pass along the arguments. But I'm
hoping there are much more elegant solutions to be had.

Have fun!


[1]: http://rubyworks.github.com
53581739a445ad78250a676dabddf55f?d=identicon&s=25 James Coglan (Guest)
on 2009-10-27 00:23
(Received via mailing list)
> easiest way to make ObjectSpace Enumerable?
>
> One could always take the brute force approach and completely
> re-implement Enumerable module to pass along the arguments. But I'm
> hoping there are much more elegant solutions to be had.



There is probably something better to be had using reflection and method
generation, but here's one stab:

class << ObjectSpace
  def method_missing(sym, *args, &block)
    mod  = args.first.is_a?(Module) ? args.shift : nil
    enum = enum_for(*[:each_object, mod].compact)
    enum.__send__(sym, *args, &block)
  end
end

# e.g. puts ObjectSpace.map(Class) { |c| c.name }
C40020a47c6b625af6422b5b1302abaf?d=identicon&s=25 Stefano Crocco (crocco)
on 2009-10-27 09:10
(Received via mailing list)
On Saturday 24 October 2009, Daniel Moore wrote:
> |on to the #each method. So how does one resolve this issue? What's the
> |easiest way to make ObjectSpace Enumerable?
> |
> |One could always take the brute force approach and completely
> |re-implement Enumerable module to pass along the arguments. But I'm
> |hoping there are much more elegant solutions to be had.
> |
> |Have fun!
> |
> |
> |[1]: http://rubyworks.github.com

Here's my solution. It uses an instance variable to keep trace of the
argument
given to the enumerable method.

In more detail, here's how it worsk: after extending Enumerable, in
redefines
each method added by it allowing any number of arguments (indeed, all
that is
needed is to add one argument. I believe it can be done that way but I
don't
think it's worth the effort). The first argument is then removed from
the
array and inserted at the end of the array contained in the
@__chosen_class
instance variable (which is created if it doesn't already exist). Then,
super
is called, passing it the new argument list (that is, the one with the
first
argument removed) and the block.

The each method is then defined to either accept one argument, so that
you can
do

ObjectSpace.each(Array){...}

or to use the last element of @__chosen_class.

The reason to store the class in an array is to allow nested calls, that
is
something like this:

ObjectSpace.select(cls1) do |o|
  ...
  ObjectSpace.any?(cls2){...}
  ...
end

When ObjectSpace.any? is called, it'll push cls2 at the end of
@__chosen_class, so that its each will use that class, while each for
the
outer block will use cls1. I enclosed the call to super in a
begin/ensure
clause to be sure that the class pushed in the array is removed at the
end of
the method call.

module ObjectSpace

  methods = Enumerable.instance_methods - self.public_methods

  extend Enumerable

  methods.each do |m|

    instance_eval <<-STR

      def #{m} *args, &blk
        @__chosen_class ||= []
        @__chosen_class << args.shift
        begin super *args, &blk
        ensure @__chosen_class.pop
        end
      end

    STR

  end


  def self.each cls = nil
    each_object(cls || @__chosen_class[-1]){|o| yield o}
  end

end

Stefano
E0d864d9677f3c1482a20152b7cac0e2?d=identicon&s=25 Robert Klemme (Guest)
on 2009-10-27 11:16
(Received via mailing list)
2009/10/27 Stefano Crocco <stefano.crocco@alice.it>:
> On Saturday 24 October 2009, Daniel Moore wrote:
> Here's my solution. It uses an instance variable to keep trace of the argument
> given to the enumerable method.

Instance variables are not thread safe.  I am using a thread local
instead:

#! ruby19

# CODE

class <<ObjectSpace
  include Enumerable

  def arg(a, &b)
    stack = Thread.current[:_os_arg_] ||= []
    stack.push a

    begin
      instance_eval(&b)
    ensure
      stack.pop
    end
  end

  def each(&b)
    a = ((Thread.current[:_os_arg_].last || Object) rescue Object)
    each_object(a, &b)
    self
  end
end

# TEST

Foo = Struct.new :x
foos = (1..10).map {|i| Foo.new i}

result_test = ObjectSpace.arg Foo do
  p map {|x| x.inspect}

  ObjectSpace.arg String do
    string_count = inject(0) do |sum, o|
      raise "no string" unless String === o
      sum + 1
    end

    puts "Nested String count: #{string_count}"
  end

  map {|x| "rt #{x.inspect}"}
end

p result_test

Thread.new do
  p ObjectSpace.inject(0) {|sum,| sum + 1}
end.join

ObjectSpace.arg Foo do
  p map {|x| x.class}
end

Cheers

robert
C40020a47c6b625af6422b5b1302abaf?d=identicon&s=25 Stefano Crocco (crocco)
on 2009-10-27 11:22
(Received via mailing list)
On Tuesday 27 October 2009, Robert Klemme wrote:
> |Instance variables are not thread safe.
> |

Yes, I was aware of this problem but forgot to mention it in my
solution.

Stefano
0ea7f61aec8fee539be0cf39b7bab77c?d=identicon&s=25 Benoit Daloze (Guest)
on 2009-10-28 10:14
(Received via mailing list)
Hi, here is my try:

module ObjectSpace
    extend Enumerable

    def each(mod = nil, &block)
        if block_given?
            if mod.is_a? Module
                each_object(mod) { |o| block.call(o) }
            else
                each_object { |o| block.call(o) }
            end
        else
            to_enum
        end
    end
    module_function :each

    (Enumerable.instance_methods-[:to_enum]).select { |m|
self.method(m).arity == 0 }.each { |m|
        instance_eval <<-STR
        def #{m}(mod = nil, &block)
            if mod.is_a? Module
                objects = to_enum.select { |o| o.is_a? mod }
            else
                objects = to_enum
            end
            objects.send(:#{m}, &block)
        end
        STR
    }
end

Some tests:

p ObjectSpace.each #=> #<Enumerator: ObjectSpace:each>
p ObjectSpace.map(Numeric) { |o| o } #=> [2.718281828459045,
3.141592653589793, ...]

p ObjectSpace.map(Class) { |c| c.name }.select { |o| o.to_s[0]<"E" } #=>
["Complex", "Binding", ...]
p ObjectSpace.select(Class) { |o| o.to_s[0]<"E" }.map { |c| c.name } #=>
["Complex", "Binding", ...]


I'm wondering which method should implement an arg. For this I used:
(Enumerable.instance_methods-[:to_enum]).select { |m|
self.method(m).arity
== 0 }
#=> [:to_a, :sort, :sort_by, :find_all, :select, :reject, :collect,
:map,
:partition, :group_by, :all?, :any?, :one?, :none?, :min, :max, :minmax,
:min_by, :max_by, :minmax_by, :reverse_each, :take_while, :drop_while]

I think the main interest is for each and map, maybe select because it
looks
nice.

For the others, it seem more clear to me to do: ObjectSpace.select {|o|
o.is_a? mod}

Something I don't understand is why it seem impossible to modify each if
Enumerable is included

Cheers
45196398e9685000d195ec626d477f0e?d=identicon&s=25 Thomas Sawyer (7rans)
on 2009-11-01 21:27
(Received via mailing list)
On Oct 28, 4:13 am, Benoit Daloze <erego...@gmail.com> wrote:

> Something I don't understand is why it seem impossible to modify each if
> Enumerable is included

Not sure what you mean. Could you explain further?
45196398e9685000d195ec626d477f0e?d=identicon&s=25 Thomas Sawyer (7rans)
on 2009-11-01 21:52
(Received via mailing list)
I was impressed with these solutions. The use of an instance variable
to store the argument I found particularly clever.

I had worked on something like this a long time ago. At the time I
completely reimplemented Enumerable by hand to accept #each arguments
(it was the first time I ever used TDD, btw). I later realized
afterward a bit of meta-programming could make all of it a whole lot
easier, so I created what is now enumargs.rb (gem install enumargs).
It is similar to Benoit's solution.

  require 'enumerator'

  # This is a simple reimplementation of the core Enumerable module
  # to allow the methods to take and pass-on arbitrary arguments to
the
  # underlying #each call. This library uses Enumerator and scans
  # Enumerable so it can alwasy stay in sync.
  #
  # NOTE Any Enumerable method with a negative arity cannot pass
arguments
  # due to ambiguity in the argument count. So the methods #inject and
#zip
  # do NOT work this way, but simply work as they do in Enumerable.
  # However the method #find, and it's alias #detect, have been made
to work
  # by removing its rarely used optional parameter and providing
instead an optional
  # keyword parameter (:ifnone => ...). Please keep these difference
in mind.
  #
  #   require 'enumargs'
  #
  #   class T
  #     include Enumerable::Arguments
  #     def initialize(arr)
  #       @arr = arr
  #     end
  #     def each(n)
  #       arr.each{ |e| yield(e+n) }
  #     end
  #   end
  #
  #   t = T.new([1,2,3])
  #   t.collect(4)
  #   #=> [5,6,7]
  #
  module Enumerable
  module Arguments

    def self.wrap_enumerable_method( methodname )

      m = methodname
      meth = Enumerable.instance_method(m)
      arity = meth.arity

      case arity <=> 0
      when 0
        class_eval %{
          def #{m}( *args, &yld )
            enum_for(:each, *args).#{m}( &yld )
          end
        }
      when 1
        class_eval %{
          def #{m}( *args, &yld )
            args, each_args = args[0...#{arity}], args[#{arity}..-1]
            enum_for(:each, *each_args).#{m}( *args, &yld )
          end
        }
      else
        class_eval %{
          def #{m}( *args, &yld )
            enum_for(:each).#{m}( *args, &yld )
          end
        }
      end
    end

    Enumerable.instance_methods(false).each do |m|
      wrap_enumerable_method( m )
    end

    # Make exception for #find (a negative arity method) to accept
    # keyword argument.
    #
    #   ObjectSpace.find(Class, :ifnone=>lambda{1}) { |e| ... }
    #   ObjectSpace.find(Class, :ifnone=>lambda{1}) { |e| ... }
    #
    def find(*args, &yld)  # future use **keys ?
      if Hash === args.last and args.last.key?(:ifnone)
        ifnone = args.last.delete(:ifnone)
        args.pop if args.last.empty?
        enum_for(:each, *args).find( ifnone, &yld )
      else
        enum_for(:each, *args).find( &yld )
      end
    end
    alias_method :detect, :find

  end
  end

With that, the solution to #222 is simply:

  class << ObjectSpace
     include Enumerable::Arguments
     alias each each_object
  end

Eg.

  ObjectSpace.select(Class){ |c| c < Exception }

Thanks.
33117162fff8a9cf50544a604f60c045?d=identicon&s=25 Daniel X Moore (yahivin)
on 2009-12-04 08:01
(Received via mailing list)
Attachment: 222.tar.gz (3 KB)
(Sorry for the delays!)

There were many great solutions to this week's quiz.

James Coglan provides us with a compact solution using `method_missing`.

    class << ObjectSpace
      def method_missing(sym, *args, &block)
        mod  = args.first.is_a?(Module) ? args.shift : nil
        enum = enum_for(*[:each_object, mod].compact)
        enum.__send__(sym, *args, &block)
      end
    end

When calling an `Enumerable` method like `map` or `each` on
`ObjectSpace` it will trigger `method_missing` as James's solution
does not have `ObjectSpace` include the `Enumerable` methods directly.
The first argument, if present and a `Module` will be extracted. An
enumerator is then created using `enum_for` and passing the given
`Module`, if present, to the `each_object` method of `ObjectSpace`.
The original method invoked that brought us into method missing in
then invoked on the new enumerator with the remaining arguments and
block. This will work for `map` as well as `inject` as long as the
first argument is a `Module`. Methods such as `map` and `each` may be
called with no `Module` given, in which case the block is called once
for each living, nonimmediate object in the Ruby process. The density
of meta-programming in James's solution is a paragon of virtue and a
good test of one's meta-programming knowledge.

Benoit Daloze's solution uses meta-programming to define new methods
that accept a `Module` parameter for all the 0 arity methods in
`Enumerable`. `each` is also redefined to accept a `Module` parameter,
though this is not used by the redefined methods, they use `to_enum`
and `select` to filter out all instances of the given `Module`.

    objects = to_enum.select { |o| o.is_a? mod }

Robert Klemme added an `arg` method to `ObjectSpace` which stored the
parameter in a thread local stack variable, so that calls may be
nested. Robert's `arg` method takes a block which is evaluated within
the context of `ObjectSpace`. The `each` method is defined to call
`each_object` with the parameter given by the top of the thread local
stack variable.

Stefano Crocco and Intransition also gave some good solutions, view
the attachment[1] for full details.

Thank you everyone for you solutions to this week's quiz. It was fun
and really mind stretching. Keep the suggestions coming!

[1]: http://rubyquiz.strd6.com/quizzes/222.tar.gz
This topic is locked and can not be replied to.