0

In my Ruby application, I want to clone a class so that I can make some slight changes to the clone without affecting the original class (see the note below for details). Unfortunately, the cloned class isn't behaving the way I would expect. Specifically, class methods of the cloned class seem to have trouble accessing constants and class variables. Observe:

irb(main):001:0> class Foo
irb(main):002:1>   HELLO = "Hello, world!"
irb(main):003:1>   def self.say_hello
irb(main):004:2>     HELLO
irb(main):005:2>   end
irb(main):006:1>   def self.cls_var=(val)
irb(main):007:2>     @@cls_var = val
irb(main):008:2>   end
irb(main):009:1>   def self.cls_var
irb(main):010:2>     @@cls_var
irb(main):011:2>   end
irb(main):012:1> end
=> nil
irb(main):013:0> Foo.say_hello
=> "Hello, world!"
irb(main):014:0> Foo.cls_var = "Test"
=> "Test"
irb(main):015:0> Foo.cls_var
=> "Test"
irb(main):016:0> Bar = Foo.clone
=> Bar
irb(main):017:0> Bar.say_hello
NameError: uninitialized constant Class::HELLO          # ???
        from (irb):4:in `say_hello`
        from (irb):17
        from C:/Ruby193/bin/irb:12:in `<main>`
irb(main):018:0> Bar.cls_var = "Another test"
(irb):7: warning: class variable access from toplevel   # Say what?
=> "Another test"
irb(main):019:0> Bar.cls_var
(irb):10: warning: class variable access from toplevel
=> "Another test"
irb(main):020:0> Foo.cls_var
=> "Another test"                                       # Why???

What's going on here, and how do I fix this so that Bar works exactly the same as Foo does after I clone it?

Note: This question is a follow up to In Ruby, is there a way to 'override' a constant in a subclass so that inherited methods use the new constant instead of the old?


Update: Sorry guys, I guess I wasn't very clear about why I want to do this. So in my case, Foo is a class in a gem which has functionality thats nearly identical to what I want for one of my classes. In fact, the only difference between Foo and what I want is that pesky HELLO constant. I want MyClass.say_hello to return "Hello, Bob!" instead of "Hello, World!". (And before you suggest just overriding say_hello, in my case Foo has lots of other methods that use HELLO and say_hello is much more complicated than it is in my example.)

Now I could just change Foo::HELLO with Foo::HELLO.slice!(0, 7) << "Bob!", but that changes the behavior of the gem, which I don't want. So how would I create an exact duplicate of Foo that has a different value for HELLO?

TLDR: Foo is part of a gem so I don't want to edit the source. I want a class that behaves exactly the same way Foo does, except with HELLO set to a different value.

Community
  • 1
  • 1
Ajedi32
  • 45,670
  • 22
  • 127
  • 172
  • Is there a reason you're cloning rather than subclassing? – tadman Nov 05 '12 at 15:42
  • @tadman The reason I'm not just subclassing is that I'm trying to create a class that has almost the exact same functionality as the cloned method, but with different constants. (See my note in the question above.) – Ajedi32 Nov 05 '12 at 15:51
  • I tried to make sense from your question but I still don't understand why you do not subclass. Subclass is for inheriting all functionality while allowing you to change some part of the class. What you ask for is exactly sub-classing – SwiftMango Nov 05 '12 at 15:58
  • 1
    @texasbruce Except that in this example if I make `Bar` a subclass of `Foo` and then redefine `Bar::HELLO`, it won't change the behavior of `Foo.say_hello`, which is what I want. – Ajedi32 Nov 05 '12 at 16:07
  • You shouldn't be overriding constants. There's a reason they're called "constant". Instead you should have class methods that perform the same function as constants and override those. Constants are a handy way of doing what you want, but they have limitations. Method definitions are much easier to manipulate. – tadman Nov 05 '12 at 16:10
  • Since the gem is calling a global HELLO, you can hack around by setting HELLO=Foo::HELLO. – calvin Nov 05 '12 at 16:17
  • So it seems like the consensus here is that trying to clone a class is a very bad idea? – Ajedi32 Nov 05 '12 at 16:31
  • *In my comment above, I meant to say that I want it to change the behavior of `Bar.say_hello`, not `Foo.say_hello`. – Ajedi32 Nov 05 '12 at 16:33
  • Ajedi, if you don't mind me asking, why don't you want to edit the source of the gem? – sunnyrjuneja Nov 05 '12 at 19:32
  • @SunnyJuneja I don't want to have to maintain a separate version of the gem in my application. It would be kind of annoying to have to clone the gem from github, apply my custom changes, and then rebuild and reinstall the gem every time I want to update it. I'd much rather be able to just type `bundle update`. – Ajedi32 Nov 06 '12 at 17:22

5 Answers5

2

I noticed the constants were listed after the clone. Foo.constants and Bar.constants both show [:HELLO].

Adding self to your class method seemed to work.

 class Foo
   HELLO = "Hi"
   def self.say_hello
     self::HELLO
   end
 end
Alex D
  • 29,755
  • 7
  • 80
  • 126
calvin
  • 347
  • 1
  • 4
  • That's a good idea, but in my case `Foo` is part of a gem so there's no easy way for me to modify its source. – Ajedi32 Nov 05 '12 at 16:41
  • @Ajedi32 The change to the class is only effective during your script life cycle and the scope is only inside your script. It would not change the actual code in the gem. – SwiftMango Nov 05 '12 at 17:26
  • @texasbruce So you're suggesting I override every method in the class with my own code just so I can clone it? =/ `Foo` is incredibly simplified in this example; in reality the method I want to clone has multiple complex methods that depend on `HELLO`. – Ajedi32 Nov 06 '12 at 17:28
1

The semantics of cloning classes in Ruby simply don't work the way you think they should. You can get around the problem with constants by using a class method:

class Foo
  def self.say_hello
    "Hello, world!"
  end
end

(Or else use @MichaelDodge's answer.)

You will not be able to share values between cloned classes using class variables. If you have a legitimate reason to want to share a value, you will have to use some other mechanism to do so.

In one of your comments, you mentioned that the reason you want to clone and modify classes is because they are part of a gem. In that case, why don't you just fork the gem and modify its source as required?

Alex D
  • 29,755
  • 7
  • 80
  • 126
  • I don't really want to share values between the cloned classes, I just want `Bar` to work exactly the same way `Foo` does. If I modify `Bar` though, (overriding a method, changing a constant, etc) I don't want it to change `Foo`. That's why I'm cloning it. – Ajedi32 Nov 05 '12 at 16:11
0

Why not simply subclass Foo?

class Foo
  HELLO = 'hi'
end

Foo::HELLO
#=> "hi"

class Bar < Foo; end

Bar::HELLO
#=> "hi"

class Bar < Foo
  HELLO = 'hello there'
end

Bar::HELLO
#=> "hello there"
Patrick Oscity
  • 53,604
  • 17
  • 144
  • 168
  • This doesn't affect how the constants are referenced inside methods unless the methods are redefined to use `self::HELLO`. – Zach Kemp Nov 05 '12 at 15:53
  • The reason I'm not just subclassing is that I'm trying to create a class that has almost the exact same functionality as the cloned method, but with Bar's class methods accessing different constants. (See my note in the question above.) – Ajedi32 Nov 05 '12 at 15:53
  • Subclassing does exactly that. The subclass behaves exactly as the superclass, but can be altered/extended in its functionality where you need it. – Patrick Oscity Nov 05 '12 at 15:54
  • @ZachKemp Exactly! And the class I am attempting to clone is from a gem, so I can't just modify it to use `self::HELLO` instead of `HELLO`. (Not easily anyway.) – Ajedi32 Nov 05 '12 at 15:56
  • as @ZachKemp and others said, then you need to use `self::HELLO` inside your methods – Patrick Oscity Nov 05 '12 at 16:13
  • @padde But I can't do that because `Foo` is defined as part of a gem. -_- Yeah, I'm in a pretty bizarre situation. Maybe I should just copy the source of `Foo` right out of the gem and paste it into a new class. – Ajedi32 Nov 05 '12 at 16:27
0

So you can set it in the original class first, and then reset it back so it would not hurt other part of the code:

class Foo
  HELLO = "Hello, world!"
  def self.say_hello
    HELLO
  end
  def self.cls_var=(val)
    @@cls_var = val
  end
  def self.cls_var
    @@cls_var
  end

end

In your script:

#set new hello
class Foo
  OLD_HELLO = HELLO
  HELLO = "NEW HELLO WORLD"
end

class Bar < Foo
end

Bar.say_hello
#output: => "NEW HELLO WORLD"

#reset hello back
class Foo
  HELLO = OLD_HELLO
end
SwiftMango
  • 15,092
  • 13
  • 71
  • 136
  • The problem with that is that after you set `Foo`'s hello back to `OLD_HELLO` `Bar.say_hello` once again returns `"Hello, world!"`. – Ajedi32 Nov 06 '12 at 17:32
0

So it seems the consensus here is that there's no easy way of making clone work the way you might expect in this context. There are a lot of alternative solutions, but none of them work exactly the way you might expect clone to.


First of all, it's possible to fix the problem with the constants by editing the original class to refer to self::HELLO in place of just HELLO:

# Before:
class Foo
  HELLO = "Hello, world!"
  def self.say_hello
    HELLO
  end
end
Bar = Foo.clone
Bar.say_hello # Error

# After:
class Foo
  HELLO = "Hello, World!"
  def self.say_hello
    self::HELLO
  end
end
Bar = Foo.clone
Bar.say_hello # => "Hello, world!"

Unfortunately, this solution doesn't resolve the problems with class variables, and it requires you to edit the source of Foo, which might not be desirable if Foo is part of a gem or other external library.


Another solution is to subclass Foo instead of cloning it:

class Foo
  HELLO = "Hello, world!"
  def self.say_hello
    HELLO
  end
end

class Bar < Foo
end
Bar.say_hello # => "Hello, world!"

The problem with this is that redefining Bar::HELLO won't affect the result of Bar.say_hello, as you might expect from a cloned class:

Bar.const_set :HELLO, "Hello, Bar!"
Bar.say_hello # => "Hello, world!"

All in all, the most effective solution is probably to just copy the source code of Foo into another class manually. This isn't dynamic, but the result is exactly the same as what you might expect from clone.

Ajedi32
  • 45,670
  • 22
  • 127
  • 172