How do I handle a variable number of arguments pas

2020-04-08 14:14发布

I like creating functions which take an unlimited number of arguments, and being able to deal with them as a list. It's been useful to me when creating binary trees & I'm using it for a variation on the nearest-neighbour algorithm right now. My method, however, is really horrible: since I can't think of a way to iterate over an improper list (which may well be improper & degenerate), I tried using various list functions to force the improper list into list form.

This is my best attempt in a simple function to determine difference between map-nodes (works, just not sure why it works):

(define distance-between
  (lambda xs
    (let ([input-list (list* xs null)])
      (letrec ([f (lambda (xs acc)
                    (if (null? (cdr xs))
                        acc
                        (f (cdr xs) (+
                                     (abs (- (map-node-x (car xs)) (map-node-x (cadr xs))))
                                     (abs (- (map-node-y (car xs)) (map-node-y (cadr xs))))
                                     acc))))])                   
       (f (car input-list) 0)))))

As you can see, it's an ugly solution and involves some of what seems like magic to me - why is the improper list coerced into list form when I include it in a list*? (note: this sentence is misleading, this does not occur).

I'd rather have a pretty solution and no magic. Can anyone help?

For example a typical input would be:

(distance-between (map-node 1 2) (map-node 2 3) (map-node 3 4))

with the expected result:

4

(a distance of 2 between map-node (a) and m-n (b), plus a distance of 2 between map-node (b) and map-node (c)).

Alternatively one might simply input:

(distance-between (map-node 1 2) (map-node 2 2))

and get an answer of:

1

If I attempted this on the raw input, without my (let ([input-list...])...) statement, it would cause an error as (? not actually sure why given response to this question).

The function works as expected.

3条回答
做个烂人
2楼-- · 2020-04-08 14:28

Your list is not improper. When your argument is not a pair, like (lambda xs body ...) or (define (fun . xs) body ...) all your arguments gets slurped into a list. Eg.. (fun 1 2 3) would make xs '(1 2 3). Doing (list* '(1 2 3) '()) makes '((1 2 3) which you undo right away by calling your loop with car which makes it '(1 2 3) again.

Other than that your procedure works as intended. You might clean up your procedure a little, but since there is no list comprehensions that glides over a list folding over the two next elements it won't become much smaller. Below is basically the same code, but abstracting out the procedure that does the work (which if existed a foldl-pair you could have used) and with a named let as a iterator loop (which is syntactic sugar for a letrec+call).

(define (distance-between e1 . lst)
  (define (add-diff-acc e1 e2 acc)
    (+ (abs (- (map-node-x e1) (map-node-x e2)))
       (abs (- (map-node-y e1) (map-node-y e2)))
       acc))

  (let iterate ((e1 e1) (lst lst) (acc 0))
    (if (pair? lst)
        (let ((e2 (car lst)))
          (iterate e2 (cdr lst) (add-diff-acc e1 e2 acc)))
        acc)))

EDIT: About syntax sugar, named let and letrec.

(let ((x 10) (y 19)) 
  body)

is syntactic sugar for a anonymous procedure call

((lambda (x y) 
    body) 
  10 19)

A named let is just giving that procedure a name, though as if by letrec, making a recursive binding. you call it with the name you give and the arguments will be what you supply instead of the initial value in the let. I'm used to them and prefer them today. It might take some time to get used to though.

Most of the code we write is syntactic sugar for some lower level stuff. The macros are nested so that your letrec form could get reduced down lambdas eventually. The whole procedure without syntactic sugar would look like this:

(define distance-between 
  (lambda (e1 . lst)
    ((lambda (add-diff-acc)
       ((lambda (iterate e1 lst acc) ; emulate Y to substitute `letrec`
          (iterate iterate e1 lst acc))
        (lambda (iterate e1 lst acc)
          (if (pair? lst)
              ((lambda (e2)
                 (iterate iterate e2 (cdr lst) (add-diff-acc e1 e2 acc)))
               (car lst))
              acc))
        e1 lst 0))
     (lambda (e1 e2 acc)
       (+ (abs (- (map-node-x e1) (map-node-x e2)))
          (abs (- (map-node-y e1) (map-node-y e2)))
          acc)))))
查看更多
兄弟一词,经得起流年.
3楼-- · 2020-04-08 14:39

There's nothing improper about the list received as a variadic argument list (meaning: variable number of arguments). For example:

(define test-list
  (lambda xs
    (length xs))) ; xs is a normal list, use it like any other list

(test-list 1 2 3 4)
=> 4

In the above example, the xs parameter is a normal, plain, vanilla list, there's nothing improper about it. You can iterate over it as you would over any other list. There's no need to car it, it's already a list! Also, notice that the same function can be written like this:

(define (test-list . xs)
  (length xs))   ; xs is a normal list, use it like any other list

Just for reference: an improper list is one that does not end with the null list. For example: '(1 2 3 . 4). Again, that's not how a variadic argument list looks.

查看更多
▲ chillily
4楼-- · 2020-04-08 14:49

I also don't understand how your variadic argument list could be improper.

But to answer your original question (how to iterate over a possibly improper list, somewhat more elegantly), here is one way using match:

#lang racket

(define (properly-sum-improper-list xs)
  (let loop ([acc 0]
             [xs xs])
    (match xs
      [(list) acc]
      [(cons x more) (loop (+ acc x) more)]
      [x (+ acc x)]))) ;last item of improper list

(require rackunit)
(check-equal? (properly-sum-improper-list '(1 2 3 4))   10)
(check-equal? (properly-sum-improper-list '(1 2 3 . 4)) 10)

However needing to do this, at all, is probably an indication you want to fix or change something else.

查看更多
登录 后发表回答