Functional variant of 'oneof' function in Racket

前端 未结 3 1646
终归单人心
终归单人心 2020-12-20 04:31

I have written following function to find if one and only one of 5 variables is true:

(define (oneof v w x y z)
  (or (and v (not w) (not x) (not y)  (not z         


        
相关标签:
3条回答
  • 2020-12-20 04:45

    You can literally count how many true values you have in a list of arbitrary length, if that number is 1 then we're good (remember that in Scheme any non-false value is considered true). Also notice how to create a procedure with a variable number of arguments, using the dot notation:

    (define (oneof . vars)
      (= 1
         (count (lambda (v) (not (false? v)))
                vars)))
    

    For example:

    (oneof #f #f #f #f #t)
    => #t
    (oneof #f #f #f #f #f)
    => #f
    (oneof #t #t #t #t #t)
    => #f
    
    0 讨论(0)
  • 2020-12-20 04:48

    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.

    0 讨论(0)
  • 2020-12-20 04:55

    Another solution, more concise, is the following:

    (define (oneof . vars)
      (= 1 (count identity vars)))
    
    0 讨论(0)
提交回复
热议问题