Recently I started wondering how Boolean algebra actually worked in Hy. I knew that we followed usual Python conventions and was sort of aware of them, but decided to dig into deeper and see how things actually are.
Granted, Boolean algebra maybe doesn’t sound the most exciting subject, but I found the results interesting. This post looks into things if Python 3.4 and 0.12.1 development version of Hy are used.
So, to get started, we have two Boolean values True and False. But in addition to that, we have truthy and falsey values too. These are values that aren’t inherintly Boolean, but evaluate to either True or False when used in Boolean context (ie. in if statement). Python docs gives us a handy table showing how this works. Following values are considered False:
- number 0, regardless of the type
- empty sequences
- empty mappings (ie. empty dictionary)
- class instances where __bool__ returns False or __len__ returns 0
The last one was new to me, so I already learned something new. For older Pythons, that magic method is called __nonzero__.
Here’s where things get a more interesting. There’s few Boolean operators in Hy, namely and, or, xor and not. Out of these, and and or are variadic, meaning they can accept zero or more arguments. xor accepts only two and not only one.
For example, and works as follows:
=> (and True False) False => (and True True True) True => (and) True
Pretty standard, albeit with varying amount of arguments, right? But see what happens when we throw in truthy and falsey values:
=> (and [1 2 3] [4 5 6]) [4 5 6] => (and  [4 5 6]) 
And actually doesn’t return True or False. If all arguments are truthy, the last argument is returned. In other cases, the first falsey argument is returned as shown below.
=> (and [1 2 3] (, 4 5)  True [4 5 6] None) 
Before wandering futher, how can I know this is intentional and not just a happy coincidence? Answer is found from the code of course. And not from the implementation code as it just defines how things currently work. The answer is of course found from the tests, that very explicitly state how the system is supposed to work. So we peek at language.hy and find the following:
(defn test-and  "NATIVE: test the and function" (setv and123 (and 1 2 3) and-false (and 1 False 3) and-true (and) and-single (and 1)) (assert (= and123 3)) (assert (= and-false False)) (assert (= and-true True)) (assert (= and-single 1)) ; short circuiting (setv a 1) (and 0 (setv a 2)) (assert (= a 1)))
The very first assert tells us that the last truthy argument should be returned, while the second one seems to tell us that the first falsey argument is returned. So we can be pretty sure this is how the language is supposed to work.
So, what about or?
=> (or True True False) True => (or 0 0 2 0 5) 2 => (or False False 0) 0 => (or) None
The last statement doesn’t actually print None as the REPL just ignores None values. It seems that first truthy argument is returned or if there isn’t one, the last falsey argument is returned. Lets peek the tests again:
(defn test-or  "NATIVE: test the or function" (setv or-all-true (or 1 2 3 True "string") or-some-true (or False "hello") or-none-true (or False False) or-false (or) or-single (or 1)) (assert (= or-all-true 1)) (assert (= or-some-true "hello")) (assert (= or-none-true False)) (assert (= or-false None)) (assert (= or-single 1)) ; short circuiting (setv a 1) (or 1 (setv a 2)) (assert (= a 1)))
First assert tells us that the first truthy argument is returned, but for the last falsey argument there isn’t a clear test.
Also, notice how both or and and short circuit. They stop evaluating their parameters as soon as final result can be given. This is handy when evaluating a long list of arguments or when evaluating them is especially slow.
xor is a bit more complicated:
=> (xor 1 0) 1 => (xor  [1 2 3]) [1 2 3] => (xor 0 )  => (xor 1 1) False
The truthy argument is returned if and only if one of the values is truthy and another one is falsey. If both arguments are falsey, the latter argument is returned. But if both values are truthy, False is returned. This is because Hy can’t really figure out what falsey value would correspond to given truthy arguments.
not is similar. It simply inverts truth value of the argument, producing True for falsey argument and False for truthy argument.
=> (not 1) False => (not 0) True
All this means, that one can write clever code like below to select one of multiple parameters to process (emphasis on clever, which usually means code isn’t particularly great to maintain in the long run):
=> (defn double-up [a b c] ... (map (fn [x] (* x 2)) ... (or a b c))) => (list (double-up  [1 2 3] )) [2 4 6]
Or one can get really creative and define new Boolean semantics for custom classes (in this case for thimblerig):
=> (defclass Thimble  ... "Thimble for thimblerig" ... (defn --init-- [self name content] ... "Initialize thimble and maybe place something inside" ... (setv self.name name) ... (setv self.content content)) ... ... (defn --bool-- [self] ... "Does this thimble have pea?" ... (= self.content "pea")) ... ... (defn --repr-- [self] ... "Print out thimble" ... (.join " " [self.name "with" self.content "inside it"]))) => => (setv thimble-1 (Thimble "first thimble" None)) => (setv thimble-2 (Thimble "second thimble" None)) => (setv thimble-3 (Thimble "third thimble" "pea")) => => (or thimble-1 thimble-2 thimble-3) third thimble with pea inside it
This is definitely starting to cross to clever category (ie. hard to maintain in the future), but nevertheless it might be a good idea to remember this. It’s probably like goto: often not such a good idea, but indispensable when used correctly for correct problem.