Forum: Ruby Tricky problem with ===

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.
Erwin A. (Guest)
on 2007-05-27 07:14
(Received via mailing list)
I have a "BlankState" type of class and it's method_missing forwards
everything to an instance variable.  This way I can treat the object
specially in my own code and all other code doesn't need to know the
difference.  For a contrived example:

class Proxy
  # remove methods inherited by Object, etc
  instance_methods.each do |m|
    undef_method m unless m =~ /^__/
  end

  def initialize name, value
    @name, @value = name, value
  end

  def to_myformat
    "#{@name}: #{@value}"
  end

  def method_missing name, *args, &block
    @value.send name, *args, &block
  end
end

object = "regular string"
x = Proxy.new("UID", object)

x.class # => Array
x.size  # => 5
x.to_myformat # => UID: regular string

While that's a pretty boring example, the idea might come in handy for
something like a graph node, or linked list node (etc). Anyway, the
problem I have is the way this works:

case x
when Array puts "Array"
when Proxy puts "Proxy"
end

This calls Array.===(x) which doesn't call x.class, and it returns
false. If I redefine Array.===(other) to test self == other.class then
it works. I can't think of any way to get this to work since it
doesn't appear there are any methods of x being called that I can
intercept. Does ruby get the class 'internally' instead of calling
x.class? I guess if worse comes to worse I can use if x.is_a? Array;
...; elsif x.is_a? Foo; ...; end. I don't want to go about redefining
every class's === method, that seems to be a mess.

Erwin
Morton G. (Guest)
on 2007-05-27 09:05
(Received via mailing list)
On May 26, 2007, at 11:13 PM, Erwin A. wrote:

>
>  end
> end
>
> object = "regular string"
> x = Proxy.new("UID", object)
>
> x.class # => Array
> x.size  # => 5
> x.to_myformat # => UID: regular string

What I see when I evaluate your code is:

    x.class # => String
    x.size  # => 14

> While that's a pretty boring example, the idea might come in handy for
> something like a graph node, or linked list node (etc). Anyway, the
> problem I have is the way this works:
>
> case x
> when Array puts "Array"
> when Proxy puts "Proxy"
> end

If you really have to do this kind of thing, a work-around you might
try is:

    result = case x.class.new
             when Array then "Array"
             when String then "String"
             when Proxy then "Proxy"
             end
    result # => "String"

That's kind of ugly, but then I think using case statements to
distinguish the class of an object is kind of ugly under any
circumstances.

Regards, Morton
Robert K. (Guest)
on 2007-05-27 13:10
(Received via mailing list)
On 27.05.2007 07:04, Morton G. wrote:
>>    undef_method m unless m =~ /^__/
>>  def method_missing name, *args, &block
>>    @value.send name, *args, &block
>>  end
>> end
>>
>> object = "regular string"
>> x = Proxy.new("UID", object)

Erwin, are you aware of delegate?

>> something like a graph node, or linked list node (etc). Anyway, the
>    result = case x.class.new
>             when Array then "Array"
>             when String then "String"
>             when Proxy then "Proxy"
>             end
>    result # => "String"
>
> That's kind of ugly, but then I think using case statements to
> distinguish the class of an object is kind of ugly under any circumstances.

It's not only ugly it probably also does not make much sense because the
OP wants to test "x" and not "x.class.new".

A better solution would be to use the second form of "case" to have more
control over the test

case
   when Array == x.class
   when String == x.class
end

An alternative way is to build custom testers:

ARRAY = lambda {|x| Array === x.class}
def ARRAY.===(o) self[o] end

case x
   when ARRAY
...
end

Viewing this more abstract: maybe case isn't even the right thing to do.
  We would have to know more about what the OP is trying to accomplish.

On an even more general level there is a certain contradiction between
having something behave exactly like an Array - but also different...
:-)

Kind regards

  robert
Erwin A. (Guest)
on 2007-05-27 17:24
(Received via mailing list)
On 5/27/07, Robert K. wrote:
> Erwin, are you aware of delegate?

Yes, but it's not transparent as I'd like. I don't want the delegating
class to answer methods like class and is_a? for example. It also
doesn't work when trying to distinguish the class with a case
construct.

On 27.05.2007 07:04, Morton G. wrote:
> What I see when I evaluate your code is:
>
>    x.class # => String
>    x.size  # => 14

My mistake, I had an array to begin with and didn't remember to change
the results to match when I changed it to a string.

> A better solution would be to use the second form of "case" to have more
> control over the test
>
> case
>    when Array == x.class
>    when String == x.class
> end

That's not bad. I like it... and it's a little more explicit, too. I
didn't know you could use case that way, that seems pretty useful.

> Viewing this more abstract: maybe case isn't even the right thing to do.
>   We would have to know more about what the OP is trying to accomplish.

Well the idea is to wrap an object with this proxy class and not mess
up any code that doesn't know about it... it should seem completely
transparent. It gives you a separate namespace for methods and
instance variables, so you don't have to worry about redefining those
with a mixin or singleton method, but you can still specialize some
object by wrapping it.

> I think using case statements to distinguish the class of an object is
> kind of ugly under any circumstances.

I think it works pretty well, but I'd like to know what you prefer
instead. I realize distinguishing objects by class is not encouraged,
but I find myself needing it often, for better or worse.

> On an even more general level there is a certain contradiction between
> having something behave exactly like an Array - but also different... :-)

Yes, that's a good observation.  Though I'm very happy with how close
I've gotten to making it happen.
Stefan R. (Guest)
on 2007-05-27 18:24
Erwin A. wrote:
> case x
> when Array puts "Array"
> when Proxy puts "Proxy"
> end

Ugly, but you could try:
puts case [x.class]
  when [Array]: "Array"
  when [Proxy]: "Proxy"
  else "Hu?"
end

Regards
Stefan
Ezra Z. (Guest)
on 2007-05-28 00:35
(Received via mailing list)
On May 27, 2007, at 7:24 AM, Stefan R. wrote:

>   else "Hu?"
> end
>
> Regards
> Stefan


  case uses === to match when clauses. Therefor you can do it like this:

case x
when Array
   puts "Array"
when Proxy
   puts "Proxy
end

Cheers-

-- Ezra Z.
-- Lead Rails Evangelist
-- removed_email_address@domain.invalid
-- Engine Y., Serious Rails Hosting
-- (866) 518-YARD (9273)
Stefan R. (Guest)
on 2007-05-28 02:49
Ezra Z. wrote:
>   case uses === to match when clauses. Therefor you can do it like this:

Which doesn't work for him because his class isn't really an Array, just
pretends to be. He outlined that in his initial post.
If I misunderstood his initial post, feel free to correct me.

Regards
Stefan
Ezra Z. (Guest)
on 2007-05-28 02:55
(Received via mailing list)
On May 27, 2007, at 3:49 PM, Stefan R. wrote:

> Stefan
>
> --
> Posted via http://www.ruby-forum.com/.
>

  Yeah I think i jumped in without reading what he really wanted. My
apologies.

Thanks
-- Ezra
unknown (Guest)
on 2007-05-28 03:08
(Received via mailing list)
Hi --

On Sun, 27 May 2007, Erwin A. wrote:

>>
>>    when Array == x.class
> up any code that doesn't know about it... it should seem completely
> but I find myself needing it often, for better or worse.
But you're running aground on exactly why it isn't encouraged: namely,
it doesn't give you definitive information about the object's behavior
or interface.  Are you sure a mixin wouldn't make more sense?


David
Erwin A. (Guest)
on 2007-05-28 07:08
(Received via mailing list)
On 5/27/07, removed_email_address@domain.invalid 
<removed_email_address@domain.invalid> wrote:
>> I think it works pretty well, but I'd like to know what you prefer
>> instead. I realize distinguishing objects by class is not encouraged,
>> but I find myself needing it often, for better or worse.
>
> But you're running aground on exactly why it isn't encouraged: namely,
> it doesn't give you definitive information about the object's behavior
> or interface.

Say you have some method that accepts various objects that represent a
time. It's job is to return true if the given time is more than a week
old. We have DateTime, Time, Date, and probably some more I don't know
about. We can test that these objects have a comparison operator, and
not pay attention to their class, but that doesn't help me because
Date.new < Time.now doesn't work. In fact, Date's <=> checks if it was
passed an Integer or a Date object, because knowing the object's class
is necesarry to know how to compare them.

I don't see any other way to write that method, am I missing
something? It seems the most definitive information about any objects
behavior is its source code, next to that we can ask what class it is
and look up the documentation. I agree distinguishing objects by class
has its problems, like Date#<=> doesn't work with anything but Dates
and Integers (when maybe it should be testing respond_to? :ajd when it
isn't given an Integer), but is there a better alternative?

> Are you sure a mixin wouldn't make more sense?

Instead of using a proxy? It's a trade-off at this point between
getting case to work and having to write code that can't use intstance
variable names like @id or @name because the original object might
already be using them, or having to alias an existing #name as
#old_name and making sure my #name doesn't break any code in that
class. I might still break something with a proxy class answering
#name, but it won't break the underlying object when it calls name on
itself. I think at this point I'll accept that case statements won't
work, but I'm going to try a few other things before giving up.

Regards,
  Erwin
Robert K. (Guest)
on 2007-05-28 13:21
(Received via mailing list)
On 27.05.2007 15:23, Erwin A. wrote:
> On 5/27/07, Robert K. wrote:
>> Viewing this more abstract: maybe case isn't even the right thing to do.
>>   We would have to know more about what the OP is trying to accomplish.
>
> Well the idea is to wrap an object with this proxy class and not mess
> up any code that doesn't know about it... it should seem completely
> transparent. It gives you a separate namespace for methods and
> instance variables, so you don't have to worry about redefining those
> with a mixin or singleton method, but you can still specialize some
> object by wrapping it.

Yes, you said so earlier.  But what business problem are you trying to
solve?

> Yes, that's a good observation.  Though I'm very happy with how close
> I've gotten to making it happen.

The problem is that it seems to be a fine line between coming close and
breaking. :-)  I'd also consider mixins or other mechanisms.  It all
depends on what problem you are trying to solve...

Kind regards

  robert
unknown (Guest)
on 2007-05-28 15:07
(Received via mailing list)
Hi --

On Mon, 28 May 2007, Erwin A. wrote:

> time. It's job is to return true if the given time is more than a week
> and look up the documentation. I agree distinguishing objects by class
> #old_name and making sure my #name doesn't break any code in that
> class. I might still break something with a proxy class answering
> #name, but it won't break the underlying object when it calls name on
> itself. I think at this point I'll accept that case statements won't
> work, but I'm going to try a few other things before giving up.

I was thinking of something like (semi-tested only):

   module DateComp
     def <(other)
       Time.parse(strftime("%m/%d/%y")) < other
     end
   end

   d = Date.new.extend(DateComp)
   your_method(d)

That way, you can use duck typing in the method itself, which means
that you don't have to change the method if you want to provide it
with other objects; you just have to prepare things on the calling
end.  You might have to have a more complex mixin for your proxy case,
but anyway, that's the kind of thing I was thinking about.


David
Stefan R. (Guest)
on 2007-05-28 15:36
unknown wrote:
> Hi --
>
> On Mon, 28 May 2007, Erwin A. wrote:
>
>> time. It's job is to return true if the given time is more than a week
>> and look up the documentation. I agree distinguishing objects by class
>> #old_name and making sure my #name doesn't break any code in that
>> class. I might still break something with a proxy class answering
>> #name, but it won't break the underlying object when it calls name on
>> itself. I think at this point I'll accept that case statements won't
>> work, but I'm going to try a few other things before giving up.
>
> I was thinking of something like (semi-tested only):
>
>    module DateComp
>      def <(other)
>        Time.parse(strftime("%m/%d/%y")) < other
>      end
>    end
>
>    d = Date.new.extend(DateComp)
>    your_method(d)
>
> That way, you can use duck typing in the method itself, which means
> that you don't have to change the method if you want to provide it
> with other objects; you just have to prepare things on the calling
> end.  You might have to have a more complex mixin for your proxy case,
> but anyway, that's the kind of thing I was thinking about.
>
>
> David

Universal Date/Time comparison is indeed difficult as neither your nor
mine (below) solution attribute for different TimeZones and/or DST.
I'd implement a universal comparison like below:

def <=>(other)
  [year, day_of_year, hour, minute, second] <=> [other.year,
other.day_of_year, other.hour, other.minute, other.second]
end

Regards
Stefan
Erwin A. (Guest)
on 2007-05-28 15:57
(Received via mailing list)
I think I may have come to a solution with

class Object
  def self.=== other
    #puts "object: #{self} == #{other}"
    other.is_a? self
  end
end

It may not work in some special cases, but it seems to do the trick.
It appears this is only called when comparing to an instance of Class,
so case x when 55; true; end doesn't involve this method. I haven't
tested this much, so I may be horribly breaking something somewhere,
but so far it seems okay.

However, I haven't gotten it to work when comparing to a Module, so
x.extend(SomeMixin); case x when SomeMixin ... doesn't work.
Module.=== doesn't seem to be involved in this test.

Regards,
  Erwin
Erwin A. (Guest)
on 2007-05-28 16:22
(Received via mailing list)
On 5/28/07, Stefan R. <removed_email_address@domain.invalid> wrote:
> Universal Date/Time comparison is indeed difficult as neither your nor
> mine (below) solution attribute for different TimeZones and/or DST.
> I'd implement a universal comparison like below:
>
> def <=>(other)
>   [year, day_of_year, hour, minute, second] <=> [other.year,
> other.day_of_year, other.hour, other.minute, other.second]
> end

I was just trying to illustrate the need for using case to determine
an object's class.  If we were to use mixins as an alternative, it's
still very complicated because we'll need to redefine Time#<=>,
Date#<=> and so on. Won't we now have to check if
other.is_a?(DateComp) and still handle the cases when
other.is_a?(self.class)... ? I don't see any difference, because now
we're relying on knowing about DateComp instead of Time or Date.

I agree mixins are useful in many cases, but maybe not in this one.
Another way to make code less rigid might be to still use the case
construct but have something that trusts the foreign object to do the
right thing in the default case.  For example

class Date
  def <=> other
    case other
    when self.class # this case is redundant, with the default case
below
      self.ajd <=> other.ajd
    when Numeric
      self.ajd <=> other
    else
      if other.respond_to? :ajd
        # hope that other.ajd means the same thing
        self.ajd <=> other.ajd
      else
        nil # causes ArgumentError (from Comparable probably)
      end
    end
  end
end

Then any class that's supposed to represent a date or time can
implement #ajd and be compatable. But we still had to check if other
was an Integer, because defining Numeric#ajd doesn't really make
sense.

Regards,

Erwin
unknown (Guest)
on 2007-05-28 16:27
(Received via mailing list)
Hi --

On Mon, 28 May 2007, Erwin A. wrote:

> I was just trying to illustrate the need for using case to determine
> an object's class.  If we were to use mixins as an alternative, it's
> still very complicated because we'll need to redefine Time#<=>,
> Date#<=> and so on. Won't we now have to check if
> other.is_a?(DateComp) and still handle the cases when
> other.is_a?(self.class)... ? I don't see any difference, because now
> we're relying on knowing about DateComp instead of Time or Date.

You don't have to test for DateComp ancestry, though.  You just assume
it (or equivalent) in your method, and then make sure that only
objects with that mixin (or equivalent) are passed to the method.
That way, you encapsulate just the operation itself in the method
(basically, duck typing), and the caller takes responsibility for only
sending along objects that fit the bill.  (Ha ha, bill, duck, get it?
Oh, never mind :-)


David
Erwin A. (Guest)
on 2007-05-28 16:57
(Received via mailing list)
On 5/28/07, Robert K. <removed_email_address@domain.invalid> wrote:
> > Well the idea is to wrap an object with this proxy class and not mess
> > up any code that doesn't know about it... it should seem completely
> > transparent. It gives you a separate namespace for methods and
> > instance variables, so you don't have to worry about redefining those
> > with a mixin or singleton method, but you can still specialize some
> > object by wrapping it.
>
> Yes, you said so earlier.  But what business problem are you trying to
> solve?

I'm not trying to solve a business problem (I'm not sure what you
mean?).  I would think this is something more like a design pattern,
like http://home.earthlink.net/~huston2/dp/proxy.html

Here's some code that implements it

class Object
  def self.=== other
    #puts "object #{self} === #{other}"
    other.is_a? self
  end
end

class Thunk
  instance_methods.each do |m|
    undef_method m unless m =~ /^__/
  end

  def initialize &block
    @proc = block
    @computed = false
  end

  def method_missing method, *args, &block
    unless @computed
      puts "computing value"
      @computed = true
      @value = @proc.call
    end
    @value.send method, *args, &block
  end
end

$stdout.sync = true

# the block isn't called until any message is sent to x, this way
# if x is never used (maybe there's a condition that prevents this)
# we'll never have to bother performing a slow computation
x = Thunk.new { sleep 5; 'expensive computation...'; 50000 }; nil

if Time.now.sec.modulo(2).zero? # even
  puts "value: #{x}"
end

Regards,
  Erwin

PS: Yes, I am aware there have been other ruby projects that do
similar things. This is just an example, where any code that's passed
a "Thunk" doesn't need to know it's anything special... unless it's
using a case statement to check if it implements a mixin ;-)
This topic is locked and can not be replied to.