Grunthor
Grunthor

Reputation: 93

Ruby method with unusual syntax

I've got some method:

def example_method
  I ♥ world!
end

I've got two questions about this method:

  1. How is it possible that this method won't return error because of non existence of quotes (example_method isn't mine method).
  2. My question is, how to make that this method will return 'I ♥ too!' string without redefining, overriding this method.

Point 2) is an exercise which I have to solve.

Thank you!

Upvotes: 2

Views: 137

Answers (2)

Jörg W Mittag
Jörg W Mittag

Reputation: 369633

How is it possible that this method won't return error because of non existence of quotes (example_method isn't mine method).

Well, how is it possible that

puts foo

is not a SyntaxError? Simply because Ruby lets you leave out the implicit receiver and the parentheses around the argument list in a message send, so this is equivalent to

self.puts(foo)

And why should there be quotes, anyway? Quotes are for String literals, but there are lots of other things besides String literals in Ruby, e.g. other literals (Integers, Floats, Hashes, Arrays, Symbols), variables, message sends, etc.

My question is, how to make that this method will return 'I ♥ too!' string without redefining, overriding this method.

Have you tried just running it? The error message will not only answer your question #1, it will also tell you how to solve your question #2.

example_method
# NoMethodError: undefined method `world!'

Okay, so it is telling us that it couldn't find a method named world!. What could we possibly do about fixing that problem? Well, how about defining one?

def world!; end

example_method
# NoMethodError: undefined method `♥'

Great, we made progress! Now it is telling us that it couldn't find a method named . So, let's define that!

def ♥; end

example_method
# ArgumentError: wrong number of arguments (given 1, expected 0)
# in `♥'

Progress again! Ruby wants us to know that doesn't take any arguments but we are passing it one. For now, just let's ignore all arguments.

def ♥(_) end

example_method
# NoMethodError: undefined method `I'

Well, we already know how to solve that one, don't we?

def I; end

example_method
# ArgumentError: wrong number of arguments (given 1, expected 0)
# in `I'

And we have seen that one, too, already:

def I(_) end

example_method
# => nil

That looks promising! Everything works, we just have to fix the return value:

def I(_)
  'I ♥ too!'
end

example_method
# => 'I ♥ too!'

And, we're done!

Notice how we didn't even have to do any real work to solve the exercise? At every step of the way Ruby told us exactly what the immediate problem was, and every single time the solution was trivial: Ruby can't find a method? Let's create one! Ruby even helpfully tells us the name of the method it cannot find, so we can just cut&paste it. Ruby tells us that we are passing arguments to a method that doesn't take any? Let's add a parameter to the method, Ruby even helpfully tells us the name of the method and how many parameters we need to add. The method is return the wrong value? Just make it return something different!

Okay, we're done, we have solved the problem. But our solution is, for lack of a better word, "boring". Now, generally, boring is good, boring means simple, boring means easy, boring means that we didn't have to do much thinking to create the code, which means that whomever ends up maintaining the code later on doesn't need to do much thinking, it means that when we are chasing down bugs, we don't need to do much thinking (or in other words: we don't need to "chase down" bugs, they will be glaringly obvious and staring us right in the face).

But in this case, it's an exercise, and exercise is meant to stimulate us.

So, what can we do? We have two methods (world! and ) that don't do anything, and we have the parameters to and I that are unused. Let's see how we can put them to good use.

We notice that the structure of our sentence resembles the structure of the code within example_method. So, one idea would be that (instead of I simply returning the whole sentence in one bit) every method is responsible for generating only that fragment of the sentence that corresponds to its position in the code. So, world! returns 'too!', returns '♥', and I returns 'I':

def world!
  'too!'
end

def ♥(_)
  '♥'
end

def I(_)
  'I'
end

example_method
# => 'I'

Hmm … okay, that's not what we wanted. What's the problem? Ah, we forgot about the arguments! We have now put our two unused methods to work, but we still haven't made use of our unused arguments. So, let's see – I takes an argument, and I must return the string 'I' followed by … something … and the only "something" we have available is our argument. Let's try what happens if we return the string 'I' followed by the argument:

def I(rest)
  'I' << rest
end

example_method
# => 'I♥'

That's better. We need to add a space, though:

def I(rest)
  'I ' << rest
end

example_method
# => 'I ♥'

And of course, we need the rest of the sentence as well, but luckily we still have an unused argument in the method:

def ♥(rest)
  '♥ ' << rest
end

example_method
# => 'I ♥ too!'

Great! We now have a solution that is no longer "boring", that actually makes use of all our methods and arguments. And it is even easily extendible, for example, if we want to make it say 'I ♥ YOU too!', we only need to make one simple change:

def YOU(rest)
  'YOU ' << rest
end

I ♥ YOU world!
# => 'I ♥ YOU too!'

There's just one thing that's bothering me. Take a look at those three methods:

def ♥(rest)
  '♥ ' << rest
end

def I(rest)
  'I ' << rest
end

def YOU(rest)
  'YOU ' << rest
end

That does look a bit repetitive. Firstly, the name of the method and the value of the string literal are always the same, so there is duplication within each method, and secondly, the three methods are all very similar and have identical structure.

One way we could get rid of the duplication within the methods is to use Ruby's reflection capabilities to get access to the name of the method within the body of the method, by using the Kernel#__callee__ method:

def ♥(rest)
  "#{__callee__} #{rest}"
end

def I(rest)
  "#{__callee__} #{rest}"
end

def YOU(rest)
  "#{__callee__} #{rest}"
end

As you can see, now the methods are not just structurally similar, they are completely identical. We could get rid of the duplication between the methods by synthesizing them dynamically instead of writing each one out explicitly:

%i[♥ I YOU].each do |method|
  define_method(method) do |rest|
    "#{__callee__} #{rest}"
  end
end

That's much better! No duplication anymore.

By the way, did you notice something? I forgot to test that our method still works! Let's do that right now, always test at every step of the way!

example_method
# => 'I ♥ too!'

I ♥ YOU world!
# => 'I ♥ YOU too!'

Phew, everything still works.

Actually, now that we dynamically synthesize the methods, there is no need to use the Kernel#__callee__ reflective method anymore, our loop knows the name of the method anyway:

%i[♥ I YOU].each do |method|
  define_method(method) do |rest|
    "#{method} #{rest}"
  end
end

example_method
# => 'I ♥ too!'

I ♥ YOU world!
# => 'I ♥ YOU too!'

One last thing we could do now, is instead of synthesizing a fixed set of methods, let's just respond to any sort of method someone throws at us. That's what method_missing is for:

def method_missing(method, rest)
  "#{method} #{rest}"
end

example_method
# => 'I ♥ too!'

I ♥ YOU world!
# => 'I ♥ YOU too!'

I very much ♥ YOU world!
# => 'I very much ♥ YOU too!'

So, here's the full code at the end of all of our refactorings:

def example_method
  I ♥ world!
end

def world!
  'too!'
end

def method_missing(method, rest)
  "#{method} #{rest}"
end

example_method
# => 'I ♥ too!'

I ♥ YOU world!
# => 'I ♥ YOU too!'

I very much ♥ YOU world!
# => 'I very much ♥ YOU too!'

Upvotes: 3

tadman
tadman

Reputation: 211750

Ruby's syntax is very forgiving. When presented with elements of the form a b c it is interpreted as chained method calls: a(b(c))

As such, that means you need to define a method a which takes a single argument, another b which takes an argument, and a method c which returns some value.

The c part should be this:

def world!
  "too!"
end

Then you can chain that with another method:

def ♥(x)
  "♥ #{x}"
end

This method name might seem impossible, but the current version of Ruby does allow unicode characters in method names.

The last part is similar, left as an exercise for you.

Upvotes: 5

Related Questions