views:

142

answers:

5

Hello, i have some problem with creating a function to my Rails app. I want it to work like this:

str = "string here"
if str.within_range?(3..30)
puts "It's withing the range!"
end

To do that i added this into my application helper:

def within_range?(range)
  if self.is_a?(String)
    (range).include?(self.size)
  elsif self.is_a?(Integer)
    (range).include?(self)
  end
end

But i get this error:

undefined method `within_range?' for "":String

Do you know what the problem is? and could you please help me. If there is a easier way please say so then.

Thanks in advance, Micke.

+4  A: 

The function does not somehow add itself to the object class. I'm going to assume you want to add it to Object since you have an is_a call in there. You have to reopen it:

class Object
  def within_range?(range)
    if is_a?(String)
      (range).include?(size)
    elsif is_a?(Integer)
      (range).include?(self)
    end
  end
end
ryeguy
+5  A: 

What you are trying is not just creating a new function. You are trying to add a new method to String.

To do that you can put your function within_range? into a new module and then include that module into the classes you want.

Normally this is done by calling the include method, but since you cannot call String.include or change the source code of the String or Integer class, you have to cheat and use the send method.

String.send(:include, YourModule)
Integer.send(:include, YourModule)

That way the String and the Integer classes get all the methods defined in YourModule.

For more in-depth information try this article: Why Ruby’s Mixins Gives Rails an Advantage over Java Frameworks

DR
okey, so should i add a new helper to my rails app or where should i put my new module and where should i add the "send" code?Sorry, im kinda new to ruby and rails
Micke
I'd put it in the lib folder and the send calls either into boot.rb or environment.rb (Careful: Changes in the lib folder require a restart before they become visible to the application)
DR
Why use send instead of String.include?
ryeguy
@ryeguy: `Module#include` is private.
Jörg W Mittag
A: 

Like it mentions, it cannot find the method called 'within_range' for the String class. You need to add the method to the String class.

class String
 def within_range?(range)
  if self.is_a?(String)
    (range).include?(self.size)
  elsif self.is_a?(Integer)
    (range).include?(self)
  end
 end
end
Pran
He wants it on the object class, not the string class. Else there wouldn't be an `is_a?` check in there.
ryeguy
@ryeguy: No, he wants two methods, one on `String` and one on `Integer`. The `is_a?` is merely duplicating Ruby's method dispatch functionality.
Jörg W Mittag
A: 

I agree that using a module is the best answer. Alternatively, and less OO, write your method this way:

def within_range?(obj, range)
  case obj
  when String  then range.include?(obj.size)
  when Integer then range.include?(obj)
  end
end

and call it like:

str = "string here"
puts "'#{str}' is within the range!" if within_range?(str, 3..30)
glenn jackman
+4  A: 

Hello, i have some problem with creating a function to my Rails app.

Uh oh, right there we run into our first misunderstanding: Ruby is not a functional programming language. It is an object-oriented programming language. There is no such thing as a function in Ruby. Ruby only has methods that live on classes.

str = 'string here'
puts "It's within the range!" if str.within_range?(3..30)

Here we can see that you obviously intend to have your method available on string objects. In order for your method to be available on string objects, it needs to be defined on the String class (or any of its ancestors).

To do that i added this into my application helper:

def within_range?(range)
  if is_a?(String)
    range.include?(size)
  elsif is_a?(Integer)
    range.include?(self) 
  end
end

Here, you are adding the within_range? method to the anymous top-level object and not to the String class. Adding a method to the anymous top-level object, also makes it "magically" available as a private method on the Object class which is the superclass of (almost) all classes. Therefore, within_range? is now available on all objects, but it is private, so you can only call it with an implicit receiver.

Which is why I don't understand why you are getting this error:

But i get this error:

undefined method `within_range?' for "":String

There's two things wrong here: first, the object that Ruby reports the error on should be "string here":String, not "":String. This means that you were actually calling the within_range? method on an empty string and not on the string 'string here' as your code sample shows. Which means that somewhere else in your code base, before the code sample that you posted here, within_range? already gets called on an empty string.

The second problem is that you are getting the totally wrong error message. Ruby tells you that Ruby couldn't find a within_range? method for strings, i.e. that there is no within_range? method on the String class or any of its superclasses including Object. But there should be! You did (although accidentally) define a private method on Object. The error message that you should be getting is this:

NoMethodError: private method `within_range?' called for "string here":String

This means that the code that defines the method never actually got executed. I am not familiar enough with Ruby on Rails to diagnose why that may be the case, though.

But now on to the contents of the code itself. Like I hinted at above, you need to add this code to the String class. And actually, inside your method, you are dealing with both strings and integers, so you need to add the code to a common superclass of String and Integer and there is only one and that is Object:

class Object
  def within_range?(range)
    if    is_a?(String)  then range.include?(size)
    elsif is_a?(Integer) then range.include?(self) end
  end
end

[I took the liberty of removing a couple of extra parentheses, selfs and newlines.]

There are a couple of problems with this. First: is_a? in Ruby is a Code Smell. It almost always means that you are either doing something wrong or at least something "un-Rubyish", like trying to re-implement static typing. (Don't get me wrong: I like static typing, but if I need it, I use a more appropriate language than Ruby.)

In general, if you find yourself using is_a? or its cousins kind_of? and Module#===, you should take a step back and reconsider your design.

Another problem with your code is that you have a condition that examines self. This is even easier to see when we refactor the code to use a case expression instead of an if expression:

class Object
  def within_range?(range)
    case self
    when String  then range.include?(size)
    when Integer then range.include?(self) end
  end
end

Here, it is easy to see that the object is looking at itself. It should never need to do that! It should always already know everything there is to know about itself. That's as if you had to look in the mirror to know what you are wearing: you shouldn't need to do that, after all you put the clothes on yourself! (And if you need to do that, it is probably the result of some bad decision the night before, just like in programming ...)

A third smell here is the fact that you are doing different things, depending on the type of an object. That's exactly what polymorphism is for: if you say foo.bar, Ruby will call a different version of bar, depending on what the type of foo is. There's no need for you to re-implement this yourself, it's already there.

So, let's step back: what is the method actually doing? Well, if it's called on a string, then it does one thing and if it's called on an integer, it does another thing. So, we are lazy and let Ruby do the work of figuring out which of the two to run:

class String
  def within_range?(range)
    range.include?(size)
  end
end

class Integer
  def within_range?(range)
    range.include?(self)
  end
end

This is already much simpler and much clearer. And there is another advantage: now, if you call within_range? on an object that it doesn't know about, it will actually tell you that it doesn't know what to do by raising a NoMethodError:

[].within_range?(3..30)
# => NoMethodError: undefined method `within_range?' for []:Array

The old version was missing an else clause, so it would have just returned nil which evaluates to false in a boolean context, which means that the error would probably have gone undetected.

There is even more we can do: there is actually nothing here that restricts the method from only working with whole numbers, it could just as easily work with floating point numbers, rationals or even complex numbers. So, instead of defining the second method on Integer, we could instead define it on Numeric.

But if you think about it: there actually isn't even anything that requires it to be a number at all, it could in fact be any object, so we could even define it on Object.

This brings me to the last two problems I have with this method, which are related to each other: I think the name for the String version is misleading, and I don't like that the method does completely different things despite its name being the same.

For an integer (or any object, if we define the method on Object), the method checks whether or not the integer (or object) itself is within the range. But for a string, the method does not check whether or not the string itself is within the range, rather it checks whether the length of the string is within the range. This is completely non-obvious and surprising to me:

  2.within_range?(1..3)     # => true
'B'.within_range?('A'..'C') # => false

I, and I suspect pretty much everybody else who reads that piece of code, would expect both of those to be true. The fact that B is not between A and C is completely confusing.

That's what I meant above: the method does two different things despite having the same name. Also, in the case of the string, the method name implies that it checks whether the string is within a certain range, but it actually checks whether the string length is within an certain range, and there is absolutely no indication in the method name that this is the case.

A much better name would be length_within_range?, and you would call it like so:

'B'.length_within_range?(1..2) # => true
'B'.within_range?('A'..'C')    # => true

[Assuming that within_range? is defined on Object.]

This makes it very clear to everyone reading the code that we are comparing the length of the string and not the string itself.

However, now there really isn't much difference between

str.length_within_range?(3..30)
str.length.within_range?(3..30)

Which means we can get rid of the string version altogether, leaving us only with the Object version.

The Object version, in turn, doesn't really do anything except flipping argument and receiver around, so we might just as well write

(3..30).include?(str.length)

and get rid of the whole thing altogether.

Jörg W Mittag
Thank you for that well written and great explonation. Now things are more clearer for me. You also made me laugh about the clothes :)As you say i really don't need that method (yeah, i learned that it is a method). I'm just new to ruby but you explained things really good so i have learned new things today, thank you!
Micke