3

Unary minus seems to have special precedence when a numeric literal is involved. Is this documented behavior?

The precedence tables I've seen (e.g. here) don't even mention the dot (method-call) operator.

Test on ruby 2.3.6:

puts "=== literal integer ==="
# `-` has higher precedence than `.`
p( -1.abs )   # => 1
p( -(1.abs) ) # => -1 (previous line should match this if `.` had higher precedence)

puts "=== literal float ==="
# again `-` has higher precedence than `.`
p( -1.2.abs )   # => 1.2
p( -(1.2.abs) ) # => -1.2 (previous line should match this if `.` had higher precedence)

puts "=== integer in a variable ==="
(1).tap do |i|
  # `.` has higher precedence
  p( -i.abs )   # -1
  p( (-i).abs ) # 1 (previous line should match this if `-` had higher precedence)
end

puts "=== float in a variable ==="
(1.2).tap do |i|
  # `.` has higher precedence
  p( -i.abs )   # -1.2
  p( (-i).abs ) # 1.2 (previous line should match this if `-` had higher precedence)
end

puts "=== literal string ==="
'a'.frozen? == false or raise "frozen_string_literal must be disabled"

# Note that unary minus on Strings returns a frozen copy if the string wasn't already frozen
# `.` has higher precedence (differs from numeric precedence)
p( (-'a'.succ).frozen? )   # true
p( ((-'a').succ).frozen? ) # false (previous line should match this if `-` had higher precedence)
Kelvin
  • 20,119
  • 3
  • 60
  • 68
  • To me it seems the literal is correct, and the non-literal precedence is too low. Perhaps I am thinking backwards about it, though :P – ForeverZer0 Aug 08 '18 at 16:04
  • @ForeverZer0 Personally, either precedence is fine, but what bothers me is the inconsistency. I don't really like needing to memorize a special case for numerics. – Kelvin Aug 08 '18 at 16:07
  • 1
    Is this the same as [**Unary operators behavior**](https://stackoverflow.com/q/22085607/479863)? Looks the same to me (with `+` instead of `-`) and the answers are the same. – mu is too short Aug 08 '18 at 16:41
  • @muistooshort yes it's the same. Thanks for that link - the `Ripper` demo was also helpful. – Kelvin Aug 08 '18 at 17:17

1 Answers1

4

The reason is that in the literal number case, the - in front isn't an unary operator, but part of the literal syntax.

However, the - operator itself has lower precedence than method invocation. Given that there is no -'string' literal syntax for strings, this rule always applies regardless of if the string was literal or not.

class Integer
  def -@
    puts 'Called'
  end
end

class String
  def -@
    puts 'Called'
  end
end

-1 # nothing, the - wasn't an unary operation, but part of the number construction
x = 1
-x # Called

-'a' # Called
a = 'a'
-a # Called

Another interesting thing is that if you put a space between the number and the -, the - is no longer part of the literal syntax.

- 1 # Called

Here is the semantic explanation:

  • There is such thing as "the number negative one". It makes sense that there should be literal syntax for it (as there is for any positive number). And the most intuitive syntax for it is -1.
  • We still want to be able to call the unary operator on literal positive numbers. The most intuitive (and easy to implement) way for that would be not to make the parser super fancy as to ignore any random amount of whitespaces in the literal syntax for negative numbers. Hence why - 1 accounts to "apply the unary minus to the number (positive) one".
  • There is no such thing as "the string negative 'a'". That is why -'a' means "apply the unary minus to the string 'a'".
ndnenkov
  • 35,425
  • 9
  • 72
  • 104
  • Nice demonstration! I don't doubt you, but do you have a reference for the precedence of method invocation generally? – Cary Swoveland Aug 08 '18 at 16:32
  • And if you start adding some spaces (https://stackoverflow.com/a/24523553/479863) it goes back to the unary operator so `-11` and `- 11` are interpreted differently (which you just noted while I was writing my comment), – mu is too short Aug 08 '18 at 16:44
  • @muistooshort, did you mean add more minutes instead of spaces? Indeed if you have `a -- b` the only logical way this expression makes sense is if the second `-` is interpreted as unary `-` for `b` and the first `-` is interpreted as binary `-` for `a` and `-b`. – ndnenkov Aug 08 '18 at 16:49
  • @CarySwoveland, I don't but seems so with simple experimentation. Given `x = 1`. `-x.abs == -1`. Meaning the operations are applied like `-(x.abs)` and not `(-x).abs`. – ndnenkov Aug 08 '18 at 16:51
  • No, spaces, hence `-11` vs `- 11`. The `a -- b` case (actually `a ++ b` but that's immaterial) is where it came up before. – mu is too short Aug 08 '18 at 17:02
  • @muistooshort, ah I see. I actually had already added this (plus why it's happening and what it means semantically) in the answer btw. – ndnenkov Aug 08 '18 at 17:13
  • You added that while I was adding my comment then I editing my comment, cheers :) – mu is too short Aug 08 '18 at 17:25
  • @muistooshort ok, my bad. – ndnenkov Aug 08 '18 at 17:26
  • 1
    Weird, but I get it. Basically `-1` (w/o space) is treated "atomically" by the parser, so the unary minus precedence rule doesn't even come into play. – Kelvin Aug 08 '18 at 17:45
  • But at the same time, `-3**2` is `-9`, so the minus in `-3` is *not* treated as "part of literal syntax" in this case. – Mike Kaganski May 30 '22 at 08:18
  • @MikeKaganski that is because [`**` has higher precedence than unary `-`](https://stackoverflow.com/questions/21060234/ruby-operator-precedence-table), hence the interpretation is actually `-(3**2)`. If you instead try with multiplication (which has lower precedence) `-3*2` it is again "part of the literal syntax". – ndnenkov May 30 '22 at 08:59
  • But the description above makes the "unary minus" argument irrelevant, since here it must not be a "unary minus operator", but a literal syntax. – Mike Kaganski May 31 '22 at 06:05