polishing ruby by ryan davis

One Way 1.9 Drives Me Nuts

Published 2013-01-23 @ 12:00

I raised a fuss when Matz proposed adding the ability to define ! and != on a class. The idea that you can contradict simple logic was befuddling and seemed like a really bad design choice. Despite many of my other proposals getting shot down with “that might confuse a developer” or “that could cause problems in [obscure edgecase]”, this one was defended with “I trust the developer to be smart”.

In 1.8, an unless statement is normalized to a negated if statement, such that the following are all equivalent:

1
2
3
4
5
6
7
8
9
a unless b

a if ! b

if b then
  nil
else
  a
end

When ruby parses code, they all wind up being treated like the 3rd form. This makes sense. You apply simple logical transformations and normalize the code.

But, this has been thrown out the window in 1.9.

Instead, in 1.9 the first and the third are equivalent:

1
2
3
4
5
6
7
8
9
a unless b

# becomes:

if b then
  nil
else
  a
end

but in the second ! goes down an entirely different code path:

1
2
3
4
5
6
7
a if ! b

# becomes:

if b.!() then
  a
end

So if you have a mix of programming styles (or a mix of programmers) you can have entirely different results. Won’t that be fun to debug?

The same is true for != vs ==. What was normalized in 1.8 as:

1
2
3
4
5
6
7
a != b

!(a == b)

# both become:

!(a.==(b))

but in 1.9:

1
2
3
4
5
6
7
8
9
a != b

!(a == b)

# become:

a.!=(b)

(a.==(b)).!

Again… a debugging nightmare. I don’t see why we have this feature. It simply seems like trouble waiting to happen.

If we’re going to allow you to contradict logic, we should at least do it in a consistent manner. Everything should normalize towards !. Such that these are all equivalent:

1
2
3
4
5
6
7
8
9
a unless b

a if ! b

# become:

if b.!() then
  a
end

The != vs == case makes less sense, honestly, since you can also normalize towards == with !… The fact that you can contradict yourself 3 ways in a single class means that there isn’t any one way to normalize. I don’t think there is a clean solution.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
require "minitest/autorun"

class HaHa
  def == o
    HaHa === o
  end

  alias != ==

  def !
    true
  end
end

describe HaHa do
  it "must be as confusing as possible" do
    assert HaHa.new == HaHa.new
    assert HaHa.new != HaHa.new
    assert HaHa.new
    assert !HaHa.new
  end
end

passes with:

# Running tests:

.

Finished tests in 0.000615s, 1626.0163 tests/s, 6504.0650 assertions/s.

1 tests, 4 assertions, 0 failures, 0 errors, 0 skips