TRUE or FALSE?: Ruby booleans are weird!
Posted: Sat Dec 19, 2020 12:48 am
Just recently, the following Ruby problem was posted...
The intent of that code seems pretty obvious. But despite there being no error messages, it doesn't do what it appears to: The if predicate is always TRUE and, even weirder, the @status and/or @note variables may get unwanted new values assigned to them!
There are multiple issues contributing to the unexpected results, so no quick way to describe why it's that way (I did quickly respond with a fixed line of code, of course!) Hence, I made this thread to discuss a bit more about how Ruby "booleans" and predicates ("if" being the most common) work, taking the above code as an example.
(NB: Watch out for example code boxes that need scrolling)
1) Like most computer languages, Ruby allows the boolean values (actually objects) true and false. These work much like you would expect if you're familiar with "green" boolean values, and you can write them directly into your code...
NB: These Ruby objects are always written in lower case. I'll try to use bold text for Ruby stuff in the text, and CAPS when I just mean the abstract concepts from boolean logic.
2) However, all Ruby objects have a boolean TRUE/FALSE equivalence. The objects false and nil are equivalent to boolean FALSE. All other Ruby objects, even just simple numbers, are equivalent to boolean TRUE.
3) To test for equality, we must use double-equals (==). In Ruby, single equals (=) always assigns a value to a variable or to an attribute of an object. However, just to complicate things, assignments in Ruby also count as expressions - that is, the assigned value is returned to the surrounding code as a "result" of the operation...
4) The "symbolic" operator ! (=NOT) has priority over && (=AND), which has priority over the || (=OR) operator (the same way that multiply/divide have priority over add/subtract in maths). However, the "English language" not, and, and or operators don't do this - they always read strictly from left to right. This means that these two styles are not always interchangeable without changing the meaning of the expression...
5) This is probably the weirdest one. Given what I've said above, what would be the result of "50 and 60" (or "50 && 60")? The (Ruby object) true, right? Nope, the result is actually 60! In fact, the result is always one of the two operands (whichever determines the final answer; the right-hand side isn't even evaluated if the left-hand side makes the answer certain). Now, to be fair, this makes no difference to an if statement (e.g. 60 is still a "true" equivalent) - but even so, what's the point of it? Well, it means that you can do something similar to using boolean "bitmasks" in DSP code...
This is a tricky technique to use, and isn't often recommended, so I'll say no more except to point out something to be careful of: even though other objects are equivalent to boolean TRUE or FALSE, they will never compare equal to the Ruby objects true or false...
Finally; what does all of this mean for the example line of code (NB: not usable code, I missed out the if's trailing lines to save space)...
Especially note that all of the rules discussed above can be applied without any errors being raised - Ruby is extremely relaxed about what you can use in boolean expressions and "if" statements! Also important is that we haven't tested the values of @status and @note - we've forced them to have the answers we were looking for!
(NB: Don't generalise the first step: how the assignment's "=" binds is another area where the priority depends on how the operators are used, and the "shortcut" when an operator's left-hand-side gives a quick answer can complicate things too).
Hopefully, it's now clearer why the original code didn't work as expected. Using what we've learned here, one possible working replacement would be...
You probably noticed that I wrote the equality tests there "backwards". When testing equality, it makes no difference logically; the reason has to do with ease of debugging...
This code also shows no errors when using midi splitter and combiner:Code: Select all
if @status = 144 and @note = 48 or 50 or 60
# [...]
end
The intent of that code seems pretty obvious. But despite there being no error messages, it doesn't do what it appears to: The if predicate is always TRUE and, even weirder, the @status and/or @note variables may get unwanted new values assigned to them!
There are multiple issues contributing to the unexpected results, so no quick way to describe why it's that way (I did quickly respond with a fixed line of code, of course!) Hence, I made this thread to discuss a bit more about how Ruby "booleans" and predicates ("if" being the most common) work, taking the above code as an example.
(NB: Watch out for example code boxes that need scrolling)
1) Like most computer languages, Ruby allows the boolean values (actually objects) true and false. These work much like you would expect if you're familiar with "green" boolean values, and you can write them directly into your code...
Code: Select all
x = true
y = false
z = x || y # Can be written: z = x or y
# z now equals true.
NB: These Ruby objects are always written in lower case. I'll try to use bold text for Ruby stuff in the text, and CAPS when I just mean the abstract concepts from boolean logic.
2) However, all Ruby objects have a boolean TRUE/FALSE equivalence. The objects false and nil are equivalent to boolean FALSE. All other Ruby objects, even just simple numbers, are equivalent to boolean TRUE.
Code: Select all
if false
# This won't run
end
if nil
# This won't run
end
if true
# This runs
end
if 3.1415927
# This runs
end
if "hello"
# This runs
end
# etc....
3) To test for equality, we must use double-equals (==). In Ruby, single equals (=) always assigns a value to a variable or to an attribute of an object. However, just to complicate things, assignments in Ruby also count as expressions - that is, the assigned value is returned to the surrounding code as a "result" of the operation...
Code: Select all
# This assigns 10 to x. It also sends 10 (the "result" of the assignment) to the output.
output (x = 10)
# This assigns 10 to x. The result of this is then assigned to y; so y is also now 10.
y = (x = 10)
# This is same as above; the "=" always binds closest to the variable on its left.
y = x = 10
# And snipped from the example code...
if @status = 144
# This assigns 144 to @status, then passes the result 144 to the "if".
# Since 144 is equivalent to TRUE, these lines will now always execute.
end
4) The "symbolic" operator ! (=NOT) has priority over && (=AND), which has priority over the || (=OR) operator (the same way that multiply/divide have priority over add/subtract in maths). However, the "English language" not, and, and or operators don't do this - they always read strictly from left to right. This means that these two styles are not always interchangeable without changing the meaning of the expression...
Code: Select all
# This means: (a AND b) OR (c AND d)
if a && b || c && d
# This means: ((a AND b) OR c) AND d
if a and b or c and d
5) This is probably the weirdest one. Given what I've said above, what would be the result of "50 and 60" (or "50 && 60")? The (Ruby object) true, right? Nope, the result is actually 60! In fact, the result is always one of the two operands (whichever determines the final answer; the right-hand side isn't even evaluated if the left-hand side makes the answer certain). Now, to be fair, this makes no difference to an if statement (e.g. 60 is still a "true" equivalent) - but even so, what's the point of it? Well, it means that you can do something similar to using boolean "bitmasks" in DSP code...
Code: Select all
x = ((a == b) && 10) || 20
# If a equals b, x now equals 10.
# If a doesn't equal b, x now equals 20.
This is a tricky technique to use, and isn't often recommended, so I'll say no more except to point out something to be careful of: even though other objects are equivalent to boolean TRUE or FALSE, they will never compare equal to the Ruby objects true or false...
Code: Select all
# Store the result of a predicate test for later...
tested = (x == y)
# Dont do this, because 'tested' may be an "equivalent object" rather than 'true'...
if tested == true
#...
end
# Just use the predicate directly...
if tested
#...
end
# To insist upon a true/false result, apply the "not" or "!" operator twice over.
tested = !!(x == y)
# NB: "Green" boolean RubyEdit outputs do this automatically!
Finally; what does all of this mean for the example line of code (NB: not usable code, I missed out the if's trailing lines to save space)...
Code: Select all
if @status = 144 and @note = 48 or 50 or 60
# Separate out the assignments (NB: "=" binds tighter than "or")
if (@status = 144) and (@note = 48) or 50 or 60
# Collapse the assignment expressions to their results...
if 144 and 48 or 50 or 60
# Order of evaluation...
if ((144 and 48) or 50) or 60
# Invoke operators (returning operands, not true/false).
if (48 or 50) or 60
# ...
if 48 or 60
# ...
if 48
# Boolean equivalence
if true
Especially note that all of the rules discussed above can be applied without any errors being raised - Ruby is extremely relaxed about what you can use in boolean expressions and "if" statements! Also important is that we haven't tested the values of @status and @note - we've forced them to have the answers we were looking for!
(NB: Don't generalise the first step: how the assignment's "=" binds is another area where the priority depends on how the operators are used, and the "shortcut" when an operator's left-hand-side gives a quick answer can complicate things too).
Hopefully, it's now clearer why the original code didn't work as expected. Using what we've learned here, one possible working replacement would be...
Code: Select all
if 144 == @status && (48 == @note || 50 == @note || 60 == @note)
You probably noticed that I wrote the equality tests there "backwards". When testing equality, it makes no difference logically; the reason has to do with ease of debugging...
Code: Select all
# Instead of this...
if x == 10
# Write this...
if 10 == x
# Because this is an easy mistake to make, but won't throw an error...
if x = 10
# Whereas this is always an error because you can't assign to a static value...
if 10 = x