rubyclassivar

Explicitly setting iVar class in Ruby (ala Obj-C)


I'm an experienced Obj-C/Java programmer, and am getting into Ruby. Obviously the fact that it's so dynamic is great (re-opening classes is awesome!) but there's one thing that bugs me/worries me for when I start writing Ruby code.

I'd be interested to know what you Ruby-ers do (if anything) to explicitly set the type of iVars in your own classes. From what I can see, you can set an iVar to any object and ruby won't complain. But if you expect a specific iVar to be of a certain type, then it can cause problems down the line. For example:

class MyString
  def initialize(myString)
    @myString = myString
  end

  def uppercase_my_string
    @myString.upcase
  end
end

st1 = MyString.new("a string!")
st1.uppercase_my_string

st2 = MyString.new(["a string"])
st2.uppercase_my_string

This code will throw a NoMethodError, since of course an array has no method upcase. Unfortunately it doesn't tell us where we really went wrong (the line above, when creating str2) so we're not helped much when debugging (if str2 happens to be created several modules away in some inconspicuous place) One natural step might be to add some checks initialize as follows:

class MyString
  def initialize(myString)
    raise TypeError, "myString iVar is not a string!" unless myString.class == String
    @myString = myString
  end
end
...same code as before

Great, now if we accidentally create a new MyString we're told how silly we were (and more importantly we're told when we do it and not when we fail. A bit of a pain to type but it's OK. My next problem is when we decide to use attr_accessors on the iVar.

class MyString
  attr_accessor :my_string
  def initialize(my_string)
    raise TypeError, "myString iVar is not a string!" unless my_string.class == String
    @my_string = my_string
  end

  def uppercase_my_string
    @my_string.upcase
  end
end

st1 = MyString.new("a string!")
st1.uppercase_my_string

st2 = MyString.new("good, it's a string")
st2.my_string = ["an array!"]
st2.uppercase_my_string

Using the setter defined, we can be really sneaky and get round the error checking in initialize. Once again this has the problem of throwing the exception in uppercase_my_string and not when we accidentally set @my_string to an array.

Finally, we could create the accessors manually and add error checking but this is a massive pain... is there a quicker and easier way to do this. Or am I just being to closed minded and not dynamic enough?

Thanks!


Aside: I know that in Obj-C you still have the same problem at runtime, but typically you'll spot the compiler error saying you're assigning an object of type array to a variable of type string (or something similar), so at least we're warned where it happens


Solution

  • In practice these sorts of type problems are actually pretty rare. If you want to be paranoid (since they really are you to get you), you can send your input through to_s to ensure that you always have a string:

    def initialize(my_string)
      @my_string = my_string.to_s
    end
    

    Then you can say MyString.new(6) and everything will work as expected. Of course, you can say MyString.new([6, 11]) and get nonsense.

    If you really want my_string to be a String you wouldn't explicitly check its class. That will cause problems if someone has subclassed String so you'd want to at least use is_a?:

    def initialize(myString)
      raise TypeError, ... unless myString.is_a? String
      @myString = myString
    end
    

    There's also a to_str method you could check:

    def initialize(myString)
      raise TypeError, ... unless myString.respond_to? :to_str
      @myString = myString.to_str
    end
    

    Implementing that method would (in some circles) indicate that your thing is String-like enough to be a String. I think calling to_s would be a better idea though, that would make things behave more as people would expect in Ruby.

    As far as your mutator problem is concerned:

    st2.my_string = ["an array!"]
    

    You don't have to let anyone write anything they want into your properties: classes aren't structs. You could only automatically define the accessor and write your own mutator to automatically slip in your to_s call:

    class MyString
      attr_reader :my_string
      def initialize(my_string)
        self.my_string = my_string
      end
    
      def my_string=(s)
        @my_string = s.to_s
      end
    
      def uppercase_my_string
        @my_string.upcase
      end
    end
    

    Basically, you don't worry about types that much in Ruby, you worry about what methods something responds to. And, if you want something specifically to be a String, you make it a String by calling the universal to_s method (which is what string interpolation, "#{x}", will do).