Arc Forumnew | comments | leaders | submitlogin
Why should zero be truthy?
4 points by akkartik 4048 days ago | 24 comments
I did some searching, but I can't seem to find any discussion about this on the arc forum: why is it a bad idea for if to treat 0 as false? There's two possible designs here:

a) Zero and nil and empty list are all identical.

b) Zero is distinct, but false-y.

Are these a bad idea just because they aren't extensible to other types? Should the empty string be false-y? The empty table?



4 points by fallintothis 4048 days ago | link

Some languages certainly overload the boolean values of different datatypes. Python's an obvious one:

  >>> map(bool, [False, 0, 0.0, 0L, 0j, "", (), {}, [], set(), frozenset(), None])
  [False, False, False, False, False, False, False, False, False, False, False, False]
  >>> class C1:
  ...     def __len__(self): return 0
  ... 
  >>> class C2:
  ...     def __nonzero__(self): return False
  ... 
  >>> map(bool, [C1(), C2()])
  [False, False]
Then of course, some languages only have two distinct boolean values, and it's an error to ask the truth value of anything else (think Java).

I dunno. When using Python, I lament that the overloading is too inexplicit. But languages with exact boolean types feel too restrictive. (Such is the curse of using multiple languages: while programming in X I want to selectively cherry-pick features from !X.) I make it by fine in Python, though. It can certainly help with code golf, but often it just makes me paranoid about my if conditions.

Usually I find myself leaning towards the "one universal false value, everything else is true" camp, just because it's highly predictable yet flexible. With distinct boolean types, you have the consistency/predictability, but not so much flexibility. In Arc (and similar), there happens to be the falsehood-creep into the empty list. I'm not sure I really like that, because it isn't maximally consistent: why aren't other empty sequences false, too? Just do away with the question by having a canonical false value all its own. Then you still get some of the code-golf benefits of having everything else be true.

My two cents.

-----

4 points by rocketnia 4047 days ago | link

"In Arc (and similar), there happens to be the falsehood-creep into the empty list. I'm not sure I really like that, because it isn't maximally consistent: why aren't other empty sequences false, too? Just do away with the question by having a canonical false value all its own. Then you still get some of the code-golf benefits of having everything else be true."

This is exactly what my preference would be too. Thanks for saying it first. :)

-----

2 points by rocketnia 4045 days ago | link

Well, this ended up leading in different directions than I expected, so I'll be more specific about my opinions here.

I like the idea of the main (if ...) semanics being just another equality check or dynamic type check: "Is this nil?" If falsiness overlaps with multiple other dynamic types, then we end up having confusing crosshatching where one extension wants to do X with any falsy value and another extension wants to do Y with any list.

Secondarily, I also see some benefit in distinguishing between () and #f, because then it's possible to dispatch on whether something is a list or a boolean. But I'm also happy if we don't have booleans at all, because then "Is this nil?" can just be a special case of "Is this a list?"

-----

2 points by thaddeus 4044 days ago | link

> But I'm also happy if we don't have booleans at all, because then "Is this nil?" can just be a special case of "Is this a list?"

However, you do lose the ability to cleanly interop with other languages or transforms.

-----

2 points by fallintothis 4045 days ago | link

An interesting turnaround happens with this philosophy, too: instead of treating "the" empty sequence as false, you can treat false as though it's an empty sequence. This is what Factor does: http://docs.factorcode.org/content/article-sequences-f.html

So maybe if Arc spelled the empty list like () and nil was the singleton false value (so that (is nil ()) was nil), then map/each/etc. could still work on nil just fine. It's just that (if () 'a 'b) would evaluate to 'a instead. Not saying it's the best way, but it's certainly an option.

-----

1 point by akkartik 4045 days ago | link

Interesting. One quibble with this idea: it doesn't matter as much that map et al work on nil if nil isn't at the end of each list.

So perhaps the reason for empty list to be special is that so many list algorithms are recursive in nature, and it's nice to be able to say "if x recurse" rather than *if !empty.x recurse". Hmm, the empty array or empty string isn't included in every array/string respectively, so perhaps it's worth distinguishing from nil in some situations..

-----

2 points by akkartik 4035 days ago | link

I just ran into a case where I wished the empty list wasn't the same as the false value. When implementing infix in wart (http://arclanguage.org/item?id=16775) I said: "Range comparisons are convenient as long as they return the last arg on success (because of left-associativity) and pass nils through."

  (a < b < c)
  => (< (< a b) c)   ; watch out if b is nil

  (< nil x)          ; should always return nil
But there's one situation where you want (< nil x) to not be nil: when x is a list and you want to perform lexicographic ordering (https://en.wikipedia.org/wiki/Lexicographical_order; http://rosettacode.org/wiki/Order_two_numerical_lists).

This problem wouldn't occur if there was a first-class boolean type; then I could use False or something for chaining comparisons.

-----

2 points by akkartik 4034 days ago | link

Ok, I'm now experimenting with a new keyword in wart called false.

a) There's still no boolean type. The type of false is symbol. (The type of nil has always been nil; maybe I'll now make it list.)

b) if treats both nil and false as false-y values.

c) nil and false are not equal.

d) Comparison operators now short-circuit on false BUT NOT nil.

I can mostly use either in place of the other. But I'm trying to be disciplined about returning false from predicates and nil from functions returning lists.

Wart now has four hard-coded symbols: nil, object, caller_scope and false.[1]

Thoughts? It was surprisingly painless to make all my tests pass. Can anybody think of bugs with this kinda-unconventional framework? If you want to try it out:

  $ git clone http://github.com/akkartik/wart
  # Optionally "git checkout 0ff47b6bce" if I later revert this experiment.
  $ cd wart
  $ ./wart
  ready! type in an expression, then hit enter twice. ctrl-d exits.
[1] fn is just a sym with a value:

  let foo fn (foo () 34)
  => (object function {sig, body})

-----

2 points by fallintothis 4033 days ago | link

Technically, my first thought was that something was broken. Hitting C-d as soon as I got the prompt:

  $ time ./wart
  ready! type in an expression, then hit enter twice. ctrl-d exits.
  => nil

  real    0m29.200s
  user    0m27.602s
  sys     0m0.000s
Anyway, I was going to test to see if you had Arc's t; but it doesn't look like it:

  (if t 'hi 'bye)
  020lookup.cc:28 no binding for t
  => bye
Note that it's trivial to add:

  (<- t 't)
  => t
  (if t 'hi 'bye)
  => hi
The reason I thought to try this was because I initially balked at maintaining false and nil at the same time with the same truth values. Then I thought of t, and suddenly the pieces clicked together: at least in part, it seems like you just want a Python-like system anyway.

Once I got the landscape laid out in my head, I started objecting to it less, because I could make sense of it. You're most of the way there:

- false is a separate, canonical false value.

- t (if you chose to have it) is a separate, canonical truth value.

- nil is an empty list, but empty lists are false.

Compare to Python's True, False, and []. The major differences being:

1. No first class boolean type. In wart, this produces more of a disconnect between t and false. t (i.e., 't) is just a normal symbol whose truth value is incidental. But false is a special, unassignable keyword.

  (<- false 'hi)
  => hi
  false
  => false
Python lacks symbols (you can't just say True = 'True), so this disconnect between symbolic value and keyword doesn't exist. There is still, however, a different sort of disconnect in Python because the "first class" boolean type gets contaminated by the int type:

  >>> False < 10
  True
  >>> 867 + True
  868
  >>> isinstance(True, int)
  True
  >>> isinstance(0, bool)
  False
  >>> True, False = 0, 1
  >>> if True: print "hi"
  ...
  >>> if False: print "hi"
  ...
  hi
2. You don't take Python's next logical leap. Since you already make the empty list false, other values become fair game, such as the thread's original idea (make 0 false), the empty string, other empty data structures, etc. But like I said before, I make do in such systems. Keeping nil falsy is really just your prerogative, if you want to avoid calls to empty? that much. ;)

-----

2 points by akkartik 4033 days ago | link

Thanks for trying it out, and for the comments! Yeah it's gotten slow :(

I hadn't realized how close to python I've gotten. Seems right given how the whitespace and keyword args are inspired by it. On rosetta code I found a cheap way to get syntax highlighting was to tag my wart snippets with lang python :)

I've been using 1 as the default truth value, and it's not assignable either. I was trying to avoid an extra hard-coded symbol, but now that I've added false perhaps I should also add true.. I'm not averse to going whole-hog on a boolean type, I'd just like to see a concrete use case that would benefit from them. pos seems a reasonable case for keeping 0 truth-y, and the fact that lists include the empty list seems a reasonable case so far to keep nil false-y. But you're right, I might yet make empty strings and tables false-y.

(True, False = 0, 1 :( That's the ugliest thing I've ever seen python allow. At least throw a warning, python! Better no booleans than this monstrosity.)

-----

2 points by rocketnia 4033 days ago | link

"pos seems a reasonable case for keeping 0 truth-y"

While I personally like 0 being truthy, I don't see this as a convincing reason.

I'd treat 'pos exactly the same way as 'find. They're even conceptually similar, one finding the key and the other finding the value. For 'find, the value we find might be falsy, so truthiness isn't enough to distinguish success from failure. The same might as well be true for 'pos.

---

"But you're right, I might yet make empty strings and tables false-y."

What if the table is mutable? That's an interesting can of worms. :)

JavaScript has 7 falsy values, all of which are immutable. If we know something's always falsy, we also know it encodes a maximum of ~2.8 bits of information--and usually much less than that. It takes unusual effort to design a program that uses all 7 of those values as distinct cases of a single variable.

This means if we have a variant of Arc's (and ...) or (all ...) that short-circuits when it finds a truthy value, we don't usually have to worry about skipping over valuable information in the falsy values.

If every mutable table is falsy as long as it's empty, then a falsy value can encode some valuable information that a practical program would care about, namely the reference to a particular mutable table.

---

"(True, False = 0, 1 :( That's the ugliest thing I've ever seen python allow. At least throw a warning, python! Better no booleans than this monstrosity.)"

There's some rationale here:

http://www.python.org/dev/peps/pep-0285/

http://docs.python.org/2/whatsnew/2.3.html

http://www.python.org/download/releases/2.2.1/NEWS

The PEP describes the design and rationale of introducing booleans to Python this way. Version 2.3 implements this. Version 2.2.1 preemptively implements bool(), True, and False to simplify backporting from 2.3.

Notably, the variable names "True" and "False" were chosen to be similar to the variable name "None", and all three of these are just variables, not reserved words.

Later, version 2.4 made it an error to assign to None:

http://docs.python.org/2/whatsnew/2.4.html

From what fallintothis says, apparently the same change hasn't been made for True and False.

-----

2 points by akkartik 4033 days ago | link

Hmm, so how is one expected to check for list membership in arc? Ah, this would seem to be the canonical idiom:

  (aif (mem f seq)
    <operate on car.it>)

-----

2 points by rocketnia 4033 days ago | link

Oh, very nice! ^_^ I remember using this a few times, but yours looks much better:

  (aif (pos f seq)
    <operate on seq.it>)

-----

1 point by akkartik 4033 days ago | link

Thanks a bunch for the python links, especially the last one. They were most illuminating.

I think the error is that None, False and True were ever 'constants' rather than literals.

Update: ah, this is fixed in python 3.

-----

2 points by akkartik 4031 days ago | link

So I emailed Guido van Rossum with this question and he was nice enough to respond :)

http://python-history.blogspot.com/2013/11/story-of-none-tru...

Couldn't have done it without you, rocketnia.

-----

2 points by rocketnia 4033 days ago | link

"I was trying to avoid an extra hard-coded symbol"

Speaking of which, why are you making false and nil count as symbols at all?

I suppose it gives them an external representation without coining a new syntax like #f.

-----

1 point by akkartik 4033 days ago | link

Yeah, I'm just minimizing how much I need to change, picking my poison between hard-coded symbols and extra cell types.

-----

1 point by akkartik 4025 days ago | link

I've added some messages to at least set expectations on how slow it is:

  $ wart
  g++ -O3 -Wall -Wextra -fno-strict-aliasing boot.cc -o wart_bin        # (takes ~15 seconds)
  starting up...       (takes ~15 seconds)
  ready! type in an expression, then hit enter twice. ctrl-d exits.

-----

2 points by akkartik 4048 days ago | link

Ah, I thought of one reason:

  (if (pos 1 '(1 2 3))
    ..)
pos can return 0 on success. We need to be able to distinguish that from nil for failure.

So it turns out that empty string or empty table actually have a stronger case for being false than 0 does.

-----

2 points by rocketnia 4048 days ago | link

If you're talking about treating 0 as falsy, then why not redesign 'pos in the process? I would happily use a 'pos that worked like this:

  > (pos 1 '(1 2 3))
  (0)

-----

2 points by rocketnia 4048 days ago | link

Oh, there's also the issue with 'find. It's already cumbersome to search a list of booleans for false and a list of lists for an empty list, and now it would be difficult to search a list of numbers for zero.

Of course, we could just do the same thing as 'pos:

  > (find 0 '(1 2 0 3))
  (0)

-----

3 points by thaddeus 4048 days ago | link

Because zero is a real value that often returns as data.

-----

2 points by rocketnia 4048 days ago | link

So is an empty list. So is boolean false.

-----

2 points by thaddeus 4048 days ago | link

Yeah, but practically speaking:

  (aif (get-account-balance)
       (if (>= it 0)
           (disp it)
           (err "ouch"))
       (start-new-account))
Zero is really just as common as any other data, so why not make 1 or 2 false-y?

-----