ruby unless bug with multiple conditions

8.9k views Asked by At

Common Code:

class ThingA end
class ThingB end
class ThingC end

In order to set up conditional checks for the above types, I used the basic "if !..." construct that produced the accurate results as expected.

Sample Code if ! ...:

obj = ThingA.new
puts 'yes it is a ThingC' if !obj.is_a?(ThingA) && !obj.is_a?(ThingB) # works ok
# stdout => nothing

obj = ThingB.new
puts 'yes it is a ThingC' if !obj.is_a?(ThingA) && !obj.is_a?(ThingB) # works ok
# stdout => nothing

obj = ThingC.new
puts 'yes it is a ThingC' if !obj.is_a?(ThingA) && !obj.is_a?(ThingB) # works ok
# stdout => yes it is a ThingC

Considering the fact that "unless" is a more descriptive alternative to the basic "if !..." construct, I implemented the above using "unless", instead.

Sample Code unless:

obj = ThingA.new
puts 'yes it is a ThingC' unless obj.is_a?(ThingA) && obj.is_a?(ThingB) # BUG
# stdout => yes it is a ThingC

obj = ThingB.new
puts 'yes it is a ThingC' unless obj.is_a?(ThingA) && obj.is_a?(ThingB) # BUG
# stdout => yes it is a ThingC

obj = ThingC.new
puts 'yes it is a ThingC' unless obj.is_a?(ThingA) && obj.is_a?(ThingB) # ???
# stdout => yes it is a ThingC

Evidently, the "unless" version fails to produce the identical accurate results.

Based upon these simple and straight-forward results, would it be hard for anyone to conclude that "multiple conditions are not handled by unless accurately"?

4

There are 4 answers

0
anquegi On

The problem here is with the logic and or or, when you negate a logical function the equivalent is the opossite and the precedence of operator: the logic functions before the modifier

De Morgan's laws provide a way of distributing negation over disjunction and conjunction :

¬ ( a ∨ b ) ≡ ( ¬ a ∧ ¬ b ), 

and ¬ ( a ∧ b ) ≡ ( ¬ a ∨ ¬ b )

In your case,

(not(a) and not(b)) then you negate it => not(a or b)

and unless => not if

irb(main):001:0> class ThingA end
class ThingB end
class ThingC end
=> nil
irb(main):002:0> => nil
irb(main):003:0> => nil

irb(main):004:0> obj = ThingA.new
puts 'yes it is a ThingC' unless obj.is_a?(ThingA) || obj.is_a?(ThingB

obj = ThingB.new
puts 'yes it is a ThingC' unless obj.is_a?(ThingA) || obj.is_a?(ThingB) 

obj = ThingC.new
puts 'yes it is a ThingC' unless obj.is_a?(ThingA) || obj.is_a?(ThingB) 

<ThingA:0x000000025fc060>
    irb(main):005:0> => nil
<ThingB:0x00000002749d00>
    irb(main):009:0> => nil
<ThingC:0x000000027be420>
    irb(main):013:0> yes it is a ThingC
    => nil
0
Awlad Liton On

I do not see any bug here. Output you have got is logical. Let me breakdown your code.

obj = ThingA.new
puts 'yes it is a ThingC' unless obj.is_a?(ThingA) && obj.is_a?(ThingB) # BUG

obj.is_a?(ThingB) is false. so false && obj.is_a?(ThingB) is false. unless false is true. So it prints.

obj = ThingB.new
puts 'yes it is a ThingC' unless obj.is_a?(ThingA) && obj.is_a?(ThingB) 

obj.is_a?(ThingA) is false. false && obj.is_a?(ThingB) is false. unless false is true. So it prints.

obj = ThingC.new
puts 'yes it is a ThingC' unless obj.is_a?(ThingA) && obj.is_a?(ThingB)

obj.is_a?(ThingA) is false. false && obj.is_a?(ThingB) is false.unless false is true. So it prints.

1
Eric Duminil On

Logic with unless

Boolean logic becomes hard to parse with unless. You can rewrite it with if:

  • unless a || b is equivalent to if !(a || b), which is equivalent to if !a && !b
  • unless a && b is equivalent to if !(a && b), which is equivalent to if !a || !b

Your "Bug"

obj.is_a?(ThingA) && obj.is_a?(ThingB) must be false if ThingA and ThingB are independent classes.

It could only be true if one class was a subclass of the other. In this case, you would only need to check if obj is a Subclass instance :

obj.is_a?(ThingA) && obj.is_a?(Object) is true if and only if obj is a ThingA object, so you could just write :

obj.is_a?(ThingA)

Alternatives

For your example, you should probably use case :

obj = ThingA.new

case obj
when ThingA
  puts "yes, it is a ThingA"
when ThingB
  puts "yes, it is a ThingB"
when ThingC
  puts "yes, it is a ThingC"
else
  puts "no, it is some other Object"
end

or

case obj
when ThingA, ThingB, ThingC
  puts "yes, it is a Thing A B or C"
else
  puts "no, it is some other Object"
end

or

puts "yes, it is a #{obj.class}"

They return :

yes, it is a ThingA
yes, it is a Thing A B or C
yes, it is a ThingA
0
ThunderThunder On

As I mentioned already that I wanted to set up a "Neither x AND Nor y" filter. I went for if !obj.is_a?(ThingA) && !obj.is_a?(ThingB). Not only does it work but it also conveys the intention clearly "Neither ThingA AND Nor ThingB".

Now the question arises, why did I remove all the !s from this statement, replaced if with unless and expected the new version to produce identical results? The answer lies in how some/most of us were introduced to unless in the first place. Statements like "Ruby can also perform unless tests, which are the exact opposite of if tests..." and "Think of unless as being an alternative way of expressing “if not.”" (direct quotes from The Book of Ruby by Huw Collingbourne: pg87), basically led (more like misled) me to believe that unless can be used as an alternative to if ! in any scenario. That is why I expected it to work here as well.

New Year's Resolution item 1: unless is strictly off-limits for anything involving more than one condition.

Anyone still held bent on solving the compound-conditional if ! problems with unless could do so by enclosing the whole argument within !(). For example, the above could be solved as unless !( !obj.is_a?(ThingA) && !obj.is_a?(ThingB) ).