The framing of the original question is ill-posed. It says that C treats 0 as falsey in a boolean context, but early versions of C did not have booleans. Thinking that C treats 0 as false is looking at it from a perspective deeply influenced by modern languages. The IF statement in C tests for zero, just like many assembly languages. The idea of "false" is a higher-level abstraction that was successfully back-ported to C.
My own thoughts is that what values should be falsey in a language depends on what your language is supposed to manipulate. C is a language designed for mucking about with machine data: bits, numbers, and pointers. And those are the data types that can be sensibly used in a boolean context in C. The Lisp family is designed for processing lists (it's in the name), and lists are the only type that some Lisps let be falsey (although many don't even allow that). It seems sensible to me that a fundamentally object-oriented language would treat a null object as falsey, or the empty string in a language for text processing. I dislike it when multi-paradigm languages have many falsey values.
> The idea of "false" is a higher-level abstraction that was successfully back-ported to C.
You're pushing 'back-ported' a bit much. It's just a blatant equivalence that 0 is a conceptual false, when a zero caused a particular behaviour if used in an if/while/etc.
'false' as a concept has always been in C afaik whether it had that name (via #DEFINE) or not.
In my opinion if a language has False/None/Null/Nil as first class values, then the only sane path is for everything else to be True: the number zero, NaN, empty string, empty list, empty set, etc. All of those should evaluate to True.
That opinion rules out many strongly typed languages where booleans are strictly separate from other types, just as, in such languages, floats are separate from doubles, and any assignment of a value of type T1 to a variable of type T2, or passing a variable of type T3 to a function taking an argument of type T4 requires explicit conversion, even if that conversion could be lossless.
For example, in Swift
let f: Float = 1.0
let d: Double = f
is invalid code. You have to write
let f: Float = 1.0
let d: Double = Double(f)
In such languages, true is true, false is false, and all other values are neither true nor false.
I think that, certainly for booleans, that’s the sanest choice.
I tend to think the only sane path is a type error and anything else is arbitrary. It isn’t that hard to have converters which make sense in a standard library. isNone, isNil ..
Yeah... ISWYM and it sounds good, then again it confuses the boolean concept with the list concept, the set concept etc. etc. etc. Doesn't mean it isn't useful, and maybe there's very little risk of mistakes but still.
> if a language has False/None/Null/Nil as first class values, then the only sane path is for everything else to be True
I very much disagree.
Every type should have a false value.
Note that False is a bool while None/Null are pointers/references and Nil is Lisp's empty list. (Python's empty list is [] and it also has empty tuples.) Why should those types and no others have a false?
Clojure, like Ruby, does falseyness correctly in that there's only 2 cases of false: nil and false. Everything else is true. This is easy to reason about, and it allows for nils to "flow" through your logic (especially with nil-punning where in the context of collections, nil is also an empty sequence).
I disagree - presence/absence allows reuse of clear and terse operators, reduces the need for an extra null coalescence operator when logical or can do the job as well, and enables better developer experience in places like options hashes where "not set" always means the same thing as "false".
You're talking about hard true/false, in which you are correct. The person you're replying to is talking about "Truthy"/"Falsy", which is different than the hard true/false you're talking about.
This is a baloney C-biased question. The real question is why nil is falsey (ALONG with false) in Clojure.
To my mind this is an incredibly boneheaded mistake in a modern language. Common Lisp can be excused as it is old and has an even older ancestry. nil has been false in common lisp and its ancestors since forever, but this is an accident of history, and nobody claims it to be a good idea, as it is a source of many bugs. And indeed some lisp functions have to have additional gizmos added to them to work around it.
Scheme fixed this with real true and false constants. And then clojure, um, decided to combine the worst of both worlds, by making TWO things be false.
I worked with two languages that have exactly this same behavior (Clojure and Ruby), and both of them this is not the source of any bug that I can remember [1].
Also, I would argue that Clojure as a language is much more ergonomic thanks to `nil` being `falsey`, so this is all about the trade-offs between safety and ergonomics. Considering that, IMO, Clojure without nil being falsey would lose much more than Clojure being a more safe language, I really don't seem much problem.
[1]: well, there is one, where one of our internal tooling had this function called `assoc-if` that it would assoc a key to a map if truthy, however this also meant that when the key was of boolean type and false we would also not assoc this; but we eventually introduced `assoc-some` that does what you expect.
Two things being false can be quite pleasant, but making the empty list nil isn't worth it.
Lua gets this right, `nil` is the Ground State of Being, you get it back in lieu of a value any time you try to find one.
This requires also having a `false` because logical conditions have to be streamed sometimes and you can't transport a key with a nil value by definition.
But an empty list/table being 'false' is convenient right until it isn't. It's a kludge.
Scheme's explicit #f is false and everything else is truthy takes some getting used to, but conversely look at Tcl where boolean values are any numeric value (0 is falsy) or the strings: true, false, yes, no, on, off
False and nil being two distinct values isn't Clojure's fault though. Or not directly. There are a bunch of trade offs that Clojure makes to have very direct interop with its host languages.
A good, illustrative case for why falsiness, of any kind, leads to disaster. Boolean operations should require explicit mapping to boolean types. Make the intent clear every time, instead of requiring the programmer to memorize umpteen different rules.
There's no disaster as far as I know. This is a old, well understood Lisp idiom. Clojure specifically has a ton of macros, functions and patterns around this concept, such as if-let, when-let, some->, looking up stuff in a map, just to name a few, much this goes under the umbrella of "nil-punning" and "logically true values" etc.
Caveats and boundaries: It is typically clear whether something can return a nil or accepts a nil value one versus when it doesn't. A good example would be string methods and other Java interop, where you will make sure you don't pass in a nil value, you are crossing language boundaries here so to speak. Another boundary of nil usage would be sequences, which will not evaluate to falsy, even if empty, so you explicitly would call seq or empty? on them.
The bottom line is that you are very aware of nil values and their falsiness in Clojure and that you program with them rather than against them.
>A good, illustrative case for why falsiness, of any kind, leads to disaster.
Yup. It took a decade of ES updates to overcome these issues with Javascript. The falsy zero in JS has probably created more bugs in aggregate than any other language feature in history.
I agree that using a boolean or calling a function that returns a boolean is the best case for readable code. I think that familiarity with a language leads to using the falsy logic rules without explicit boolean logic. When script programming falsy logic can be broad and useful but for software that needs to be robust explicit boolean logic can not be beat.
How about two rules, or if you absolutely insist, three?
"Values are falsey if they are `nil` or `false`, otherwise they are truthy".
It's hard for me to map this rule to 'disaster', either in principle or in practice. It means that `if number then` does what you expect, if you expect that maybe number exists, maybe it doesn't.
The “umpteen rules”, I presume, are the ones for all the languages you know and use, not just Clojure or what have you. I would wager most people don’t work in a single language bubble.
It’s a bit optimistic that we’d ever have most languages working under the same rules though, no matter how sane it is and how dubious the value of falseyness (and the terseness you can get from it) is.
Inspired by this I wrote a post on the Julia discourse forum about the arbitrariness of truthiness in programming languages: https://discourse.julialang.org/t/on-the-arbitrariness-of-tr.... Julia requires actual `true` or `false` values in conditionals. Some people are annoyed coming from languages where they can test if a list is non-empty without having to write `isempty(a)`. But I have no regrets and this is a great example of why. Languages cannot agree on truthiness because it's arbitrary and just becomes a set of random rules you have to remember and which causes nasty bugs if you get them wrong.
zero being false is fine in statically typed languages like C, because there's still only one false value available, and you wouldn't be checking for it if you weren't expecting zero to be false
in dynamic languages, wanting to take advantage of brevity tricks with number zero or empty list or empty string being falsy can seem like a good idea, but gets confusing fast-
different languages are going to have different falsy values, and you can easily get caught off guard when legitimately wanting to accept an 'empty' value
common lisp has NIL as its definition of the end of a list, which ends up working out fine since lists are so deeply embedded in the language design that it's hard to forget that they're there (any other kind of object is 'truthy', including any size of regular array)
scheme has a separate #f that is taken as the only falsy value, which is probably also fine
python and javascript are a mess, with different kinds of 'empty' or 'nothing' values being falsy whether you want them to be or not, which is the kind of helpfulness we don't need- where evaluating in boolean context means "if this value is present but not 'empty', in a way that is dependent on the objects type, and may change over time"
My own thoughts is that what values should be falsey in a language depends on what your language is supposed to manipulate. C is a language designed for mucking about with machine data: bits, numbers, and pointers. And those are the data types that can be sensibly used in a boolean context in C. The Lisp family is designed for processing lists (it's in the name), and lists are the only type that some Lisps let be falsey (although many don't even allow that). It seems sensible to me that a fundamentally object-oriented language would treat a null object as falsey, or the empty string in a language for text processing. I dislike it when multi-paradigm languages have many falsey values.
You're pushing 'back-ported' a bit much. It's just a blatant equivalence that 0 is a conceptual false, when a zero caused a particular behaviour if used in an if/while/etc.
'false' as a concept has always been in C afaik whether it had that name (via #DEFINE) or not.
For example, in Swift
is invalid code. You have to write In such languages, true is true, false is false, and all other values are neither true nor false.I think that, certainly for booleans, that’s the sanest choice.
I very much disagree.
Every type should have a false value.
Note that False is a bool while None/Null are pointers/references and Nil is Lisp's empty list. (Python's empty list is [] and it also has empty tuples.) Why should those types and no others have a false?
Deleted Comment
How many checked luggage items do you have?
nil means the user hasn't answered the question
0 means they saw the question and don't have any checked luggage.
To my mind this is an incredibly boneheaded mistake in a modern language. Common Lisp can be excused as it is old and has an even older ancestry. nil has been false in common lisp and its ancestors since forever, but this is an accident of history, and nobody claims it to be a good idea, as it is a source of many bugs. And indeed some lisp functions have to have additional gizmos added to them to work around it.
Scheme fixed this with real true and false constants. And then clojure, um, decided to combine the worst of both worlds, by making TWO things be false.
Citation needed.
I worked with two languages that have exactly this same behavior (Clojure and Ruby), and both of them this is not the source of any bug that I can remember [1].
Also, I would argue that Clojure as a language is much more ergonomic thanks to `nil` being `falsey`, so this is all about the trade-offs between safety and ergonomics. Considering that, IMO, Clojure without nil being falsey would lose much more than Clojure being a more safe language, I really don't seem much problem.
[1]: well, there is one, where one of our internal tooling had this function called `assoc-if` that it would assoc a key to a map if truthy, however this also meant that when the key was of boolean type and false we would also not assoc this; but we eventually introduced `assoc-some` that does what you expect.
Lua gets this right, `nil` is the Ground State of Being, you get it back in lieu of a value any time you try to find one.
This requires also having a `false` because logical conditions have to be streamed sometimes and you can't transport a key with a nil value by definition.
But an empty list/table being 'false' is convenient right until it isn't. It's a kludge.
I know of no other JVM-based lisp system which makes this "tradeoff". Indeed Kawa, which is rather faster than Clojure in my experience, does not.
Which leads to this link: https://groups.google.com/g/clojure/c/OnagUrQZ1NE/m/Uwm8fvak...
Where Rich comments on the thinking behind this.
Also see https://clojure.org/reference/lisps , which compares Clojure with other Lisp dialects on this and other points
Caveats and boundaries: It is typically clear whether something can return a nil or accepts a nil value one versus when it doesn't. A good example would be string methods and other Java interop, where you will make sure you don't pass in a nil value, you are crossing language boundaries here so to speak. Another boundary of nil usage would be sequences, which will not evaluate to falsy, even if empty, so you explicitly would call seq or empty? on them.
The bottom line is that you are very aware of nil values and their falsiness in Clojure and that you program with them rather than against them.
Yup. It took a decade of ES updates to overcome these issues with Javascript. The falsy zero in JS has probably created more bugs in aggregate than any other language feature in history.
"Values are falsey if they are `nil` or `false`, otherwise they are truthy".
It's hard for me to map this rule to 'disaster', either in principle or in practice. It means that `if number then` does what you expect, if you expect that maybe number exists, maybe it doesn't.
It’s a bit optimistic that we’d ever have most languages working under the same rules though, no matter how sane it is and how dubious the value of falseyness (and the terseness you can get from it) is.
Deleted Comment
in dynamic languages, wanting to take advantage of brevity tricks with number zero or empty list or empty string being falsy can seem like a good idea, but gets confusing fast- different languages are going to have different falsy values, and you can easily get caught off guard when legitimately wanting to accept an 'empty' value
common lisp has NIL as its definition of the end of a list, which ends up working out fine since lists are so deeply embedded in the language design that it's hard to forget that they're there (any other kind of object is 'truthy', including any size of regular array)
scheme has a separate #f that is taken as the only falsy value, which is probably also fine
python and javascript are a mess, with different kinds of 'empty' or 'nothing' values being falsy whether you want them to be or not, which is the kind of helpfulness we don't need- where evaluating in boolean context means "if this value is present but not 'empty', in a way that is dependent on the objects type, and may change over time"
Pattern matching in Elixir is so powerful, why would you even bother?