Enjoying a free online course, General Game Playing, given by Michael Genesereth, got me interested in classical logic and the interesting history of the Victorians who created it.
Thanks to Christine Ladd, whatâs generally known as Boolean Algebra is a very simple subject with just five operators. Laddâs reward from Johns Hopkins University in 1882 was both she and her teacher Charles Sanders Peirce got kicked out.
What Iâve been wrapping my head around doing various database projects is how sets, logicâs five operators, and relational algebra are related, leading to a couple of Aha! moments of how they creep up in various guises in computer coding.
Copulatives â§, ⨠and â¤
As is sadly common in maths, a subject most people already find intimidating is made incomprehensible by no two textbooks using the same symbols. I favour whatâs known as copula notation of â§ for and and ⨠for or. A reason these are nice is they are handy mnemonics for their set level counterparts, ⊠and âŞ.
The most difficult of the five boolean operators to understand is implication. Thanks to picking this notation, I figured out itâs simply ⤠with a corresponding â.
Boolean Algebraâs Five operators
| Name | Predicate | Set | JavaScript |
|---|---|---|---|
| Conjunction | p ⧠q | P ⊠Q | p && q |
| Disjunction | p ⨠q | P ⪠Q | p || q |
| Implication | a ⤠b | A â B | a <= b |
| Equivalence | a â b | A â B | a === b |
| Negation | p | PC | !p |
The number of operators could actually be reduced to three by
rewriting implication and equivalence in conjunctive normal form.
What is a predicate?
Predicates are functions of zero or more variables that return Boolean values. â Al Aho and Jeff Ullman, Foundations of Computer Science, Chapter 14 Predicate Logic
I tend to use Aho and Ulmanâs textbook as my âOxford Dictionaryâ for
computer jargon, and in coding a function that returns true
or false is the general definition of a predicate.
Pedantically this isnât correct since the word originated in grammar
where a predicate is the part of the clause of a sentence which is not
the subject or object, and in Prolog a predicate is a compound data
structure, what JavaScript would call an object.
To confuse things further, there are lots of different conventions
for what the two values in Boolean âtwo valueâ algebra are. In
electrical circuits, true is on and
false is off, with conjunction
being switches in series and disjunction switches in
parallel. JSON includes true and false (along
with null) as types, so itâs usually a good
choice in JavaScript. However, sometimes itâs more convenient to work
with 0 and 1.
If 1 is in the context of a predicate, JavaScript treats it as truthy, and similarly if 0 is in the context of a predicate, JavaScript treats it as falsy.
Boolean Coercion
Before I discovered JavaScriptâs builtin Boolean(p)
coercion function, I tested if something was truthy or
falsy like this:
function pred(n) {
if (n) {
return true;
}
return false;
}Returning true from an if block is
generally considered a nooby mistake in computer coding because if
n wasnât true, return true wouldnât be called
anyway, so one could simply return n; to get true or false.
But since 1 is only true in the context of say,
if (1) {...}, I thought JavaScript was an exception to this
rule. Then I discovered that rather than my pred(n)
function, I could use Boolean(n);
As explained in negation, since JavaScriptâs
unary operator ! converts truthy values to
false and falsy values to true,
!!p does the same as Boolean(p), but Iâm
ideologically opposed to multiple exclamation marks.
it ('1 is true', function () {
expect(Boolean(1)).toBe(true);
});
it ('0 is false', function () {
expect(Boolean(0)).toBe(false);
});In JavaScript, not just 1, but any value is truthy
except 0, false, null, undefined,
the empty string "", and NaN which are
falsy.
it ('2 is true', function () {
expect(Boolean(2)).toBe(true);
});
it ('-2 is true', function () {
expect(Boolean(-2)).toBe(true);
});
it ('null is false', function () {
expect(Boolean(null)).toBe(false);
});
it ('undefined is false', function () {
expect(Boolean(undefined)).toBe(false);
});
it ('NaN is false', function () {
expect(Boolean(NaN)).toBe(false);
});
it ('"any text" is true', function () {
expect(Boolean("any text")).toBe(true);
});
it ('"" is false', function () {
expect(Boolean("")).toBe(false);
});Because an empty string is false, I wrongly assumed
empty arrays and objects would be false:
it ('an empty array is true', function () {
expect(Boolean([])).toBe(true);
});
it ('an empty object is true', function () {
expect(Boolean({})).toBe(true);
});I use Array.length to check:
it('empty array is false', function() {
expect([].length > 0).toBe(false);
});
it('empty object is false', function() {
expect(Object.keys({}).length > 0).toBe(false);
});Jasmineâs logic matchers
Since Iâm using Jasmine as my testing framework, I could use some of its many matchers to avoid Boolean coercion:
- toBeFalsy()
- toBeTruthy()
- toBeLessThanOrEqual(expected)
Jasmine has too many matchers to remember, with lots of synonyms like
toBeTrue() for toBe(true) etc. Personally I
find expect(a < b).toBe(true) easier than
expect(a).toBeLessThan(b), but I guess there are use cases
where these many options are handy.
Conjunction as in AND
Conjunction in programing is generally thought of as AND.
it ('p and q', function() {
function and(p, q) {
return (p && q);
}
expect(and(true, true)).toBe(true);
expect(and(true, false)).toBe(false);
expect(and(false, true)).toBe(false);
expect(and(false, false)).toBe(false);
});As discussed in true or 1, false or 0?,
&& only returns true and
false in the above examples because those were its inputs.
If given other truthy or falsy values, it
returns one of those with possibly unexpected results:
it('p && q returns last truthy or first falsy value', function() {
expect(2 && 1).toBe(1);
expect(1 && 2).toBe(2);
expect(null && undefined).toBe(null);
});It terms of program flow, conjunction can be thought of as nesting an
if statement inside another:
it ('and as nested ifs', function() {
function and(p, q) {
if (p) {
if (q) {
return true;
}
}
return false;
}
expect(and(true, true)).toBe(true);
expect(and(true, false)).toBe(false);
expect(and(false, true)).toBe(false);
expect(and(false, false)).toBe(false);
});Disjunction as in OR
Disjunction in programing is generally though of as OR.
it ('p or q', function() {
function or(p, q) {
return (p || q);
}
expect(or(true, true)).toBe(true);
expect(or(true, false)).toBe(true);
expect(or(false, true)).toBe(true);
expect(or(false, false)).toBe(false);
});As with && above, || only returns
true and false in the above examples because
those were its inputs. If given other truthy or
falsy values, it returns one of those with possibly
unexpected results:
it('p || q returns first truthy or last falsy value', function() {
expect(2 || 1).toBe(2);
expect(1 || 2).toBe(1);
expect(null || undefined).toBe(undefined);
});It terms of program flow, disjunction can be thought of as
if statements on the same level:
it ('or as sibling ifs', function() {
function or(p, q) {
if (p) {
return true;
}
if (q) {
return true;
}
return false;
}
expect(or(true, true)).toBe(true);
expect(or(true, false)).toBe(true);
expect(or(false, true)).toBe(true);
expect(or(false, false)).toBe(false);
});How â§ is related to âŠ
it ('AND and intersection using &&', function() {
const U = ["a","b","c","d","e","f","g","h","i",
"j","k","l","m","n","o","p","q","r",
"s","t","u","v","w","x","y","z"];
const A = ["a","b","c","d","e","f"];
const B = ["e","f","g","h","i","j"];
const I = U.filter((e) => A.includes(e) && B.includes(e));
expect(I).toEqual(["e","f"]);
});The above example is mainly for illustrative purposes. Using JavaScriptâs set intersection, we can do it without defining a universal set U:
it ('Intersection of two arrays using Set', function() {
const A = new Set(["a","b","c","d","e","f"]);
const B = new Set(["e","f","g","h","i","j"]);
expect(A.intersection(B)).toEqual(new Set(["e","f"]));
expect(B.intersection(A)).toEqual(new Set(["e","f"]));
});Another way is to think of the intersection as a subset (ie filtered array in JavaScript):
it ('Intersection of two arrays uing filter', function() {
const A = ["a","b","c","d","e","f"];
const B = ["e","f","g","h","i","j"];
expect(A.filter((e) => B.includes(e))).toEqual(["e","f"]);
expect(B.filter((e) => A.includes(e))).toEqual(["e","f"]);
});How ⨠is related to âŞ
it ('OR and union using ||', function() {
const U = ["a","b","c","d","e","f","g","h","i",
"j","k","l","m","n","o","p","q","r",
"s","t","u","v","w","x","y","z"];
const A = ["a","b","c","d","e","f"];
const B = ["e","f","g","h","i","j"];
const I = U.filter((e) => A.includes(e) || B.includes(e));
expect(I).toEqual(["a","b","c","d","e","f","g","h","i","j"]);
});Again the above is just for illustrative purposes, and in practice we would use JavaScriptâs union set operator so as to avoid defining the universal set U:
it ('Union of two arrays using Set', function() {
const A = new Set(["a","b","c","d","e","f"]);
const B = new Set(["e","f","g","h","i","j"]);
expect(A.union(B)).toEqual(new Set(["a","b","c","d","e","f","g","h","i","j"]));
expect(B.union(A)).toEqual(new Set(["a","b","c","d","e","f","g","h","i","j"]));
});
true or 1, false or
0?
As explained in the next section, an advantage of using 1 and 0
instead of true and false is the usual rules
of algebra apply to Boolean algebra. Implication is completely incomprehensible
unless p and q are thought to be 0 or 1.
Switching is fairly easy thanks to a subtlety of JavaScriptâs
&& and || operators: they arenât
strictly predicates (ie things that return true or
false), but rather polymorphic in that they return whatever
truthy or falsy values they were handed.
it ('p && q with 0 and 1', function() {
function and(p, q) {
return (p && q);
}
expect(and(1, 1)).toBe(1);
expect(and(1, 0)).toBe(0);
expect(and(0, 1)).toBe(0);
expect(and(0, 0)).toBe(0);
});
it ('p || q with 0 and 1', function() {
function or(p, q) {
return (p || q);
}
expect(or(1, 1)).toBe(1);
expect(or(1, 0)).toBe(1);
expect(or(0, 1)).toBe(1);
expect(or(0, 0)).toBe(0);
});Conjunction as in times and disjunction as in plus
Something I wished I knew back in college where I wasted time in exams writing out truth tables to check if two logic expressions were equivalent was this can be done much more quickly and easily if we think of AND as multiplication and OR as addition.
it ('and as p * q', function() {
function and(p, q) {
return p * q;
}
expect(and(1, 1)).toBe(1);
expect(and(1, 0)).toBe(0);
expect(and(0, 1)).toBe(0);
expect(and(0, 0)).toBe(0);
});
it ('or as p + q', function() {
function or(p, q) {
return p + q;
}
expect(or(1, 1)).toBe(2);
expect(or(1, 0)).toBe(1);
expect(or(0, 1)).toBe(1);
expect(or(0, 0)).toBe(0);
});And and or arenât necessarily just binary operators. As with
multiplication and addition, there could be any number of predicates to
aggregate with these operators. Thinking of or as addition
results in a bigger number than 1 which is ok in JavaScript since
anything other than 0 is true.
That in logic 1 + 1 = 1 has caused rows dating back to
the fieldâs Victorian pioneers George Boole and William
Stanley Jevons.
In 1863 Jevons wrote to Boole that surely Booleâs operation of addition should be replaced by the more natural âinclusive orâ (or âunionâ), leading to the law X+X=X. Boole completely rejected this suggestion (it would have destroyed his system based on ordinary algebra) and broke off the correspondence. â The Algebra of Logic Tradition.
I hadnât heard of Jevons until I got interested in classical logic. While his contemporary Charles Babbage became popularly known as a computing pioneer, few people have heard of Jevons even though his logic piano was actually the first digital (as in four bit) computer ever built.
The issue of whether OR can add up to more than one
creeps up in program control flow. For instance, JavaScriptâs switch
statement is a form of disjunction. A convention JavaScript adopted from
C (which Câs creators have dropped in their newer language Go) is that a
matching case needs to have a terminating
return or break, else it continues to try
match futher cases â ie ORs to possibly several values.
Usually we want OR to result in one thing or nothing, so
thatâs a common source of bugs.
Conjunction as in minimum and disjunction as in maximum
Reading Robert Kowalskiâs Logic for Problem Solving, something that confused me was he referred to âand-or treesâ as opposed to the more usual âminimax treesâ.
Then I had a bit of an epiphany: assuming one thinks in terms of 0
and 1, and is the same as min and
or the same as max, sidestepping the
1 + 1 = 1 debacle.
it ('and as min(p, q)', function() {
function and(p, q) {
return Math.min(p, q);
}
expect(and(1, 1)).toBe(1);
expect(and(1, 0)).toBe(0);
expect(and(0, 1)).toBe(0);
expect(and(0, 0)).toBe(0);
});
it ('or as max(p, q)', function() {
function or(p, q) {
return Math.max(p, q);
}
expect(or(1, 1)).toBe(1);
expect(or(1, 0)).toBe(1);
expect(or(0, 1)).toBe(1);
expect(or(0, 0)).toBe(0);
});
anding and oring
arrays of booleans
A way to handle any number of predicates to and or
or, we could put them in an array and use
reduce:
it ('anding a list of booleans true', function () {
expect([true, true, true, true]
.reduce((a, b) => a && b, true)).toBe(true);
});
it ('anding a list of booleans false', function () {
expect([true, true, true, false]
.reduce((a, b) => a && b, true)).toBe(false);
});
it ('oring a list of booleans false', function () {
expect([false, false, false, false]
.reduce((a, b) => a || b, false)).toBe(false);
});
it ('oring a list of booleans true', function () {
expect([false, false, false, true]
.reduce((a, b) => a || b, false)).toBe(true);
});A thing to note is the choice of initial value for the accumulator.
Again the similarity with or and sum is shown
in that we start with false or 0, while and is
product in that we start with true or 1.
Implication p ⤠q
Implication and equivalence differ from the other three logical operators in that instead of having booleans as their inputs, these can have numbers, text, arrays, objects, ⌠whatever as their inputs but return a boolean. Each type of input represents a different example, but the return value is always a boolean.
Initially limiting to 0 and 1 as input type, a convention Iâve adopted (which I havenât seen elsewhere) is to use p ⤠q instead of p â q. As the truth table shows, it gives the expected results for 1 and 0. Writing p ⤠q has a further advantage of again being a spiky version of its set rounded counterpart P â Q.
it ('implication', function() {
function implication(a, b) {
return a <= b;
}
expect(implication(1, 1)).toBe(true);
expect(implication(1, 0)).toBe(false);
expect(implication(0, 1)).toBe(true);
expect(implication(0, 0)).toBe(true);
});The meaning of the implication operator â may appear unintuitive, since we must get used to the notion that âfalsehood implies everything.â We should not confuse â with causation. That is, p â q may be true, yet p does not âcauseâ q in any sense. For example, let p be âit is raining,â and q be âSue takes her umbrella.â We might assert that p â q is true. It might even appear that the rain is what caused Sue to take her umbrella. However, it could also be true that Sue is the sort of person who doesnât believe weather forecasts and prefers to carry an umbrella at all times. â Al Aho and Jeff Ullman, Chapter 12 Propositional Logic
p ⤠q = p ⨠q
Implication can be rewritten in conjunctive normal form as
p ⤠q = p ⨠q
In JavaScript:
it ('implication with not and or', function() {
function implication(p, q) {
const notP = ((p === 1) ? 0 : 1);
return notP || q;
}
expect(implication(1, 1)).toBe(1);
expect(implication(1, 0)).toBe(0);
expect(implication(0, 1)).toBe(1);
expect(implication(0, 0)).toBe(1);
});This is a handy formula to remain in the domain of 0 and 1 without
switching to true and false, and can also be
used for equivalence if we think of it as reciprocal implication.
How ⤠is related to â
Here isSubsetOf helps a lot:
it ('isSubsetOf', function() {
const U = new Set(["a","b","c","d","e","f","g","h","i",
"j","k","l","m","n","o","p","q","r",
"s","t","u","v","w","x","y","z"]);
const A = new Set(["a","b","c","d","e","f"]);
const X = new Set([1,2,3,4]);
expect(A.isSubsetOf(U)).toBe(true);
expect(U.isSubsetOf(U)).toBe(true);
expect(X.isSubsetOf(U)).toBe(false);
});Implication for various types in JavaScript
The concept of âless than or equal toâ which is what logical implication boils down to comes up in many different guises in computer coding. Hereâs a list of some of JavaScriptâs implication functions:
- âstringâ.includes(âsubstringâ)
-
It has more specialised relatives
startsWith(âprefixâ)
and
endsWith(âsuffixâ).
Before I discovered this handy builtin predicate, I checked that
indexOfwasnât -1. This is much easier. - [1,2,3,4].includes(5)
- x â A in JavaScript.
- new Set([1,2,3,4]).has(5)
- According to the documentation, this is a more efficient way of doing x â A in JavaScript.
- Array.isArray(myArr)
- I find I need this type checker a lot to see if an element is a leaf or a branch in recursive tree or graph traversal.
- typeof x
-
Handy to test if a variable contains a âstringâ, ânumberâ, âbooleanâ, or
âfunctionâ. Arrays are seen as âobjectâ, which is why
isArrayis needed.
Equality
When dealing with atoms like true === true
or 1 === 1, equality is easy and obvious, ignoring all the
confusion in programing languages by one equal sign being commonly used
to allocate values to variables, two for equality, and then JavaScriptâs
three for strict
equality.
it ('"1" == 1', function() {
expect("1" == 1).toBe(true);
});
it ('"1" === 1', function() {
expect("1" === 1).toBe(false);
});When it comes to compound data structures, things get very complicated by JavaScript along with most other programing languages comparing memory addresses instead of values as explained in object and array equality.
Equality is reciprocal implication
Of the four binary operators in Boolean algebra,
implication is the only one that is not commutative,
ie which side of the operator a variable goes matters.
This allows a trick of defining equality as meaning we get the same result for switching the variables around in implication. A handy Boolean equality to remember is:
If a ⤠b ⧠b ⤠a is true, a = b
More practical is the set equivalent because we often need to check if the results of two database queries are the same irrespective of the order of results:
If A â B â§ B â A is true, A = B
it ('set equality', function() {
const A = new Set([1,2,3,4]);
const B = new Set([4,3,2,1]);
const C = new Set([1,2,3,4,5]);
expect(A.isSubsetOf(B) && B.isSubsetOf(A)).toBe(true);
expect(A.isSubsetOf(C) && C.isSubsetOf(A)).toBe(false);
});Negation
JavaScriptâs unary operator !
always returns true or false, ie it converts
truthy values to false and falsy
values to true. So an alternative way of Boolean coercion is
!!p, but I find Boolean(p) more
understandable.
Something thatâs cost me marks in a couple of online multiple choice exams is failing to spot the word ânotâ had been slipped into answers that on a quick read looked correct.
We as humans seem wired to misunderstand negatives, though that may be more of a problem for English and German speakers since latin languages like French make it clearer if a sentence is negative by repeating the ânotâ.
Because of our common trouble parsing NOT,
itâs generally good advice to write predicates in a âpositive wayâ by
avoiding !p whenever possible.
For instance, instead of
function myFunc(a) {
if (!p) {
// code goes here
}
}do
function myFunc(a) {
if (p) {
return;
}
// code goes here
}Set difference A - B
The first MooC I ever did was Jennifer Widomâs course on SQL. One of the problems, answering the question âwhich students did not apply for CSâ, illustrated the various pitfalls in what initially seems easy.
An easy way to translate the SQL to JavaScript is to treat the tables as an array of arrays. This means remembering which index number corresponds to which field. Making it an array of object literals would make referencing the columns by name easier, but this takes less space.
const Student = [
[123, 'Amy', 3.9, 1000],
[234, 'Bob', 3.6, 1500],
[345, 'Craig', 3.5, 500],
[456, 'Doris', 3.9, 1000],
[567, 'Edward', 2.9, 2000],
[678, 'Fay', 3.8, 200],
[789, 'Gary', 3.4, 800],
[987, 'Helen', 3.7, 800],
[876, 'Irene', 3.9, 400],
[765, 'Jay', 2.9, 1500],
[654, 'Amy', 3.9, 1000],
[543, 'Craig', 3.4, 2000]
];
const Apply = [
[123, 'Stanford', 'CS', 'Y'],
[123, 'Stanford', 'EE', 'N'],
[123, 'Berkeley', 'CS', 'Y'],
[123, 'Cornell', 'EE', 'Y'],
[234, 'Berkeley', 'biology', 'N'],
[345, 'MIT', 'bioengineering', 'Y'],
[345, 'Cornell', 'bioengineering', 'N'],
[345, 'Cornell', 'CS', 'Y'],
[345, 'Cornell', 'EE', 'N'],
[678, 'Stanford', 'history', 'Y'],
[987, 'Stanford', 'CS', 'Y'],
[987, 'Berkeley', 'CS', 'Y'],
[876, 'Stanford', 'CS', 'N'],
[876, 'MIT', 'biology', 'Y'],
[876, 'MIT', 'marine biology', 'N'],
[765, 'Stanford', 'history', 'Y'],
[765, 'Cornell', 'history', 'N'],
[765, 'Cornell', 'psychology', 'Y'],
[543, 'MIT', 'CS', 'N']
];I did a Venn diagram in SVG as a learning exercise:
Pitfall #1 is to think this can be solved with
Apply[2] !== "CS".
it('Apply[2] !== "CS"', function() {
const A = new Set(Apply
.filter((row) => row[2] !== "CS")
.map((row) => row[0]));
expect(A).toEqual(new Set([123,234,345,678,765,876]));
})The students with IDs 123, 345, 876 appear in the list because they applied for subjects besides âCSâ. So here again is an example of why negation is very tricky to get right.
To solve this, we need to create a subset of students who did apply for CS, and then cross them off the list which JavaScriptâs difference set operator makes easy. But this leads us to pitfall #2:
it('Apply.difference(CS)', function() {
const ApplyIDs = new Set(Apply.map((row) => row[0]));
const CS = new Set(Apply
.filter((row) => row[2] === "CS")
.map((row) => row[0]));
expect(ApplyIDs.difference(CS).toEqual(new Set([234,678,765]));
});The reason thatâs still the wrong answer is thereâs students with IDs
456, 567, 654, and 789 who havenât applied for anything yet, so should
be on the list of students who havenât applied for CS. Using the wrong
superset is a very common mistake.
it('Student.difference(CS)', function() {
const StudentIDs = new Set(Student.map((row) => row[0]));
const CS = new Set(Apply
.filter((row) => row[2] === "CS")
.map((row) => row[0]));
expect(StudentIDs.difference(CS).toEqual(new Set([234,456,567,654,678,765,789]));
});