Subject: bug#48318: (ice-9 match) does not allow distinguishing between () and #nil

From: Taylan Kammer <[email-masked]>
Date: Thu, 13 May 2021 23:21:48 +0200

On 13.05.2021 22:39, Maxime Devos wrote:
> Taylan Kammer schreef op do 13-05-2021 om 21:14 [+0200]:
>> Hi Maxime,
>>
>> I believe that match conflating () and #nil is the right thing,
>> even if the current implementation does it unintentionally.
>>
>> Those two values should be considered "the same" in most situations
>> even though (eqv? #nil '()) is false.
> 
> Conflating #nil and () is reasonable for my use case,
> though this conflation should be documented.

Agree, it should definitely be documented.

>> In fact I think they should be equal? to each other.  It feels
>> wrong that (equal? '(foo . #nil) '(foo . ())) evaluates to false,
>> even though both arguments represent the list '(foo).
> 
> The guile manual has some information on this.
> (6.24.2.1 Nil, under 6.24.2 Emacs Lisp).

Good catch.  I see it mentions that equal? is expected to be
transitive, so if (equal? #nil '()) and (equal? #nil #f) were
both true, (equal? '() #f) would have to be too, which would
be wrong for a Scheme implementation.

We could however make it equal? to just one of the two without
making equal? non-transitive.  And if we're going to do that,
I think the empty list would be the better choice, because of
the role it plays in the structure of lists.  Without any data
to verify this, I'd say that situations where #nil surprises
programmers by not being equal? to '() are likely to come up
much more often than cases where it surprises programmers by
not being equal? to #f.

The parallel between equal?-ness and external representation
also comes to mind.  I think it's not a concrete rule, but
there's the general expectation that two objects are equal?
if their external representation is the same, which is the
case for (foo . #nil) and (foo . ()), which would both be
canonically represented as (foo).

We could also go in the other direction and make Scheme's
write procedure output (foo . #nil) as (foo . #nil), but I
think that would be less desirable.

>> Please note that #nil is not ever supposed to be used intentionally.
> I know, but ...
>> It's there purely as an Elisp compatibility trick, and the only time
>> Scheme could should receive it is when receiving data generated by
>> Elisp code.  For instance when Elisp code generates a list, it would
>> be terminated by #nil.  (Which is why I think it should equal? '().)
> 
> I have been porting some common lisp code to Guile Scheme. I replaced
> '() with #nil, which allows me to largely ignore whether Lisp nil is used
> as end-of-list or as boolean for now (I'm in the process of replacing it
> with '() or #f where appropriate).

Exciting!  I guess that's one feasible extra use-case for #nil,
but as you noted it yourself, it's probably best to rewrite the
code to eliminate all the assumptions that the end-of-list object
is false as a Boolean.

> Being able to directly refer to #nil, to perform equality checks like
> (eq? #f #nil) --> #f, (eq? '() #nil) --> #f, ... can be useful for writing
> Scheme code that could be called from both elisp and Scheme when the
> compatibility isn't transparent.  For example, suppose (ice-9 match) actually
> did not conflate #nil and () (which is what I initially thought; I expected
> (ice-9 match) to compare atoms with eqv?), then the
> following code ...
> 
> ;; Somewhat contrived (untested), but based on some real code
> (define 
>   (match-lambda
>     ((_ . stuff) stuff)
>     (() 0)))
> 
> ... would need to be rewritten to something like ...
> 
> ;; Somewhat contrived (untested), but based on some real code
> (define 
>   (match-lambda
>     ((_ . stuff) stuff)
>     (() 0)
>     (#nil 0)))

Indeed.

> Also, consider the 'case' syntax.

Case is defined in terms of eqv? in the standards so I guess
it should differentiate between #nil and ().  Unlike match,
which does pattern matching on lists.

> Greetings,
> Maxime.

- Taylan