Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ruby unless bug with multiple conditions

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"?

like image 266
ThunderThunder Avatar asked Sep 19 '25 15:09

ThunderThunder


2 Answers

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
like image 60
Eric Duminil Avatar answered Sep 22 '25 22:09

Eric Duminil


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
like image 32
anquegi Avatar answered Sep 22 '25 22:09

anquegi