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, self
s 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.