1

I was playing around in irb, and noticed one cannot do 5 * "Hello".

Error

String can't be coerced into Fixnum

However "Hello"*5 provided "HelloHelloHelloHelloHello" as expected.

What is the exact reason for this? I've been looking around in the doc's and could not find the exact reason for this behavior. Is this something the designers of ruby decided?

10
  • 1
    Operators are methods, adding an answer below Commented Sep 3, 2016 at 21:20
  • 1
    Many operators are methods. Notable exceptions are =, .., ..., !, not, &&, and, ||, or, !=, !~. Commented Sep 3, 2016 at 21:26
  • Thanks for that, I didn't know that @CarySwoveland, is there a list of these somewhere? Commented Sep 3, 2016 at 21:29
  • There's no reason to expect that the instance method * should be defined on Fixnum (or one of its ancestors) merely because it's defined on String. If ['dog', 'cat', 'pig].index('cat') #=> 1 would you expect you could write 'cat'.index(['dog', 'cat', 'pig'])? Relatively few methods defined on a method's receiver are also defined on the method's argument. Commented Sep 3, 2016 at 21:37
  • 1
    @FrederickCheung, thanks for noting that. I was a bit sloppy. Here is a list for v1.9. Commented Sep 4, 2016 at 6:27

3 Answers 3

4

Basically, you are asking "why is multiplication not commutative"? There are two possible answers for this. Or rather one answer with two layers.

The basic principle of OO is that everything happens as the result of one object sending a message to another object and that object responding to that message. This "messaging" metaphor is very important, because it explains a lot of things in OO. For example, if you send someone a message, all you can observe is what their response is. You don't know, and have no idea of finding out, what they did to come up with that response. They could have just handed out a pre-recorded response (reference an instance variable). They could have worked hard to construct a response (execute a method). They could have handed the message off to someone else (delegation). Or, they just don't understand the message you are sending them (NoMethodError).

Note that this means that the receiver of the message is in total control. The receiver can respond in any way it wishes. This makes message sending inherently non-commutative. Sending message foo to a passing b as an argument is fundamentally different from sending message foo to b passing a as an argument. In one case, it is a and only a that decides how to respond to the message, in the other case it is b and only b.

Making this commutative requires explicit cooperation between a and b. They must agree on a common protocol and adhere to that protocol.

In Ruby, binary operators are simply message sends to the left operand. So, it is solely the left operand that decides what to do.

So, in

'Hello' * 5

the message * is sent to the receiver 'Hello' with the argument 5. In fact, you can alternately write it like this if you want, which makes this fact more obvious:

'Hello'.*(5)

'Hello' gets to decide how it responds to that message.

Whereas in

5 * 'Hello'

it is 5 which gets to decide.

So, the first layer of the answer is: Message sending in OO is inherently non-commutative, there is no expectation of commutativity anyway.

But, now the question becomes, why don't we design in some commutativity? For example, one possible way would be to interpret binary operators not as message sends to one of the operands but instead message sends to some third object. E.g., we could interpret

5 * 'Hello'

as

*(5, 'Hello')

and

'Hello' * 5

as

*('Hello', 5)

i.e. as message sends to self. Now, the receiver is the same in both cases and the receiver can arrange for itself to treat the two cases identically and thus make * commutative.

Another, similar possibility would be to use some sort of shared context object, e.g. make

5 * 'Hello'

equivalent to

Operators.*(5, 'Hello')

In fact, in mathematics, the meaning of a symbol is often dependent on context, e.g. in ℤ, 2 / 3 is undefined, in ℚ, it is 2/3, and in IEEE754, it is something close to, but not exactly identical to 0.333…. Or, in ℤ, 2 * 3 is 6, but in ℤ|5, 2 * 3 is 1.

So, it would certainly make sense to do this. Alas, it isn't done.

Another possibility would be to have the two operands cooperate using a standard protocol. In fact, for arithmetic operations on Numerics, there actually is such a protocol! If a receiver doesn't know what to do with an operand, it can ask that operand to coerce itself, the receiver, or both to something the receiver does know how to handle.

Basically, the protocol goes like this:

  1. you call 5 * 'Hello'
  2. 5 doesn't know how to handle 'Hello', so it asks 'Hello' for a coercion. …
  3. 5 calls 'Hello'.coerce(5)
  4. 'Hello' responds with a pair of objects [a, b] (as an Array) such that a * b has the desired result
  5. 5 calls a * b

One common trick is to simply implement coerce to flip the operands, so that when 5 retries the operation, 'Hello' will be the receiver:

class String
  def coerce(other)
    [self, other]
  end
end

5 * 'Hello'
#=> 'HelloHelloHelloHelloHello'

Okay, OO is inherently non-commutative, but we can make it commutative using cooperation, so why isn't it done? I must admit, I don't have a clear-cut answer to this question, but I can offer two educated guesses:

  1. coerce is specifically intended for numeric coercion in arithmetic operations. (Note the protocol is defined in Numeric.) A string is not a number, nor is string concatenation an arithmetic operation.
  2. We just don't expect * to be commutative with wildly different types such as Integer and String.

Of course, just for fun, we can actually observe that there is a certain symmetry between Integers and Strings. In fact, you can implement a common version of Integer#* for both String and Integer arguments, and you will see that the only difference is in what we choose as the "zero" element:

class Integer
  def *(other)
    zero = case other
    when Integer then 0
    when String  then ''
    when Array   then []
    end

    times.inject(zero) {|acc, _| acc + other }
  end
end

5 * 6
#=> 30

5 * 'six'
#=> 'sixsixsixsixsix'

5 * [:six]
#=> [:six, :six, :six, :six, :six, :six]

The reason for this is, of course, that the set of strings with the concatenation operation and the empty string as the identity element form a monoid, just like arrays with concatenation and the empty array and just like integers with addition and zero. Since all three are monoids, and our "multiplication as repeated addition" only requires monoid operations and laws, it will work for all monoids.


Note: Python has an interesting twist on this double-dispatch idea. Just like in Ruby, if you write

a * b

Python will re-write that into a message send:

a.__mul__(b)

However, if a can't handle the operation, instead of cooperating with b, it cooperates with Python by returning NotImplemented. Now, Python will try with b, but with a slight twist: it will call

b.__rmul__(a)

This allows b to know that it was on the right side of the operator. It doesn't matter much for multiplication (because multiplication is (usually but not always, see e.g. matrix multiplication) commutative), but remember that operator symbols are distinct from their operations. So, the same operator symbol can be used for operations that are commutative and ones that are non-commutative. Example: + is used in Ruby for addition (2 + 3 == 3 + 2) and also for concatenation ('Hello' + 'World' != 'World' + 'Hello'). So, it is actually advantageous for an object to know whether it was the right or left operand.

Sign up to request clarification or add additional context in comments.

Comments

3

This is because that operators are also methods(Well there are exceptions as Cary has listed in the comments which I wasn't aware of).

For example

array << 4    ==   array.<<4
array[2]      ==   array.[](2)
array[2] ='x' ==   array.[] =(2,'x')

In your example:

5 * "Hello" => 5.*("Hello")

Meanwhile

"hello" *5 => 5.*("hello")

An integer cannot take that method with a string param

If you ever dabble around in python try 5*hello and hello*5, both work. Pretty interesting that ruby has this feature to be honest.

Comments

0

Well, as Muntasir Alam has already told that Fixnum does not has a method named * which takes a string as argument. So, 5*"Hello" produces that error.But, to have fun we can actually achieve 5*"Hello" this by adding that missing method to the Fixnum class.

class Fixnum # open the class
    def * str # Override the *() method
        if str.is_a? String # If argument is String
            temp = ""
            self.times do
                temp << str
            end
            temp
        else # If the argument is not String
            mul = 0
            self.times do
                mul += str
            end
            mul
        end
    end
end

now

puts 5*"Hello" #=> HelloHelloHelloHelloHello

puts 4*5 #=> 20

puts 5*10.4 #=> 52.0

Well, that was just to show that the opposite is also possible. But that will bring a lot of overhead. I think we should avoid that at all cost.

3 Comments

Dang. This is Cool. +1!
When appending to a string try and use the in-place << to avoid creating lots of intermediate strings. Your "multiplying" of numbers by adding something on N times is completely mad, why not alias the old method and call that? Also there's no need for an explicit return in either of these blocks.
I think adding the other number N time is not as mad as you are telling. If I again write 5*4 in the else part then it will go for a recursion which will never end. I was showing that the opposite is possible. No practical use of my code is ever needed I guess and I don't know how to use alias.@tadman

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.