Your final note is accurate: it sounds like xor
is the right function but this, but it only takes two arguments. It would likely be better if xor
took any number of arguments, but given that it doesn’t, we can implement it ourselves.
Perhaps the most na?ve way would just to be to count the number of truthy values and check if that number is precisely 1. We can do this with count
or for/sum
, depending on your preference:
; using count
(define (xor . args)
(= 1 (count identity args)))
; using for/sum
(define (xor . args)
(= 1 (for/sum ([x (in-list args)])
(if x 1 0))))
Both of these work, but they don’t preserve a useful property of Racket’s xor
, which returns the single truthy element upon success rather than always returning a boolean. To do this, we could use a fold, using foldl
, foldr
, or for/fold
. However, given that we want to ideally exit as soon as possible, using the #:final
option of for/fold
is pretty convenient:
(define (xor . args)
(for/fold ([found #f])
([arg (in-list args)])
#:final (and found arg)
(if (and found arg) #f
(or found arg))))
However, this is actually still not really optimal. The two-argument version of xor
is a function, not a macro like and
and or
, because it cannot be lazy in either of its arguments. However, a many-argument xor
actually can be. In order to add this short-circuiting behavior, we can write xor
as a macro:
(define-syntax xor
(syntax-rules ()
[(_) #f]
[(_ x) x]
[(_ x rest ...)
(let ([v x])
(if v
(and (nor rest ...) v)
(xor rest ...)))]))
In general, this works just like the function versions of xor
:
> (xor #f #f #f #f #f)
#f
> (xor #f #f 1 #f #f)
1
> (xor #f #f 1 2 #f)
#f
However, like and
and or
, it sometimes “short-circuits”, not evaluating expressions if their results will not mater:
> (xor #f #f #f #f (begin (displayln "hello!") #f))
hello!
#f
> (xor #f #f 1 #f (begin (displayln "hello!") #f))
hello!
1
> (xor #f #f 1 2 (begin (displayln "hello!") #f))
#f
(Note that hello!
is never printed in the last example.)
Is this a good idea, is it a bad idea…? I don’t really know. It does seem unlikely that this behavior will ever be super useful, and it adds a lot of complexity. It also prevents xor
from being used higher-order, but you could get around that with syntax-id-rules
and expanding to the procedure version when xor
is used in an expression position. Still, it’s potentially interesting, and it makes its behavior more consistent with and
and or
, so I figured I’d include it for completeness.