Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Any good way to declare unused variables in destructuring-bind?

I can't figure, is there any way to put something like _ in erlang, for "unused value" in destructuring-bind?

For example there we have something like that:

(destructuring-bind ((_SNIPPET
                               (_TITLE . title)
                               (_DESCRIPTION . description)
                               _RESOURCE-ID (_VIDEO-ID . video-id)))) entry
                (declare (ignore 
                          _SNIPPET _TITLE _DESCRIPTION _RESOURCE-ID _VIDEO-ID))
                (list video-id title description)))

It'll be great not to put specific variable for every unused value, and write something like that:

 (destructuring-bind ((_
                         (_ . title)
                         (_ . description)
                         (_ (_ . video-id)))) entry
                    (list video-id title description)))

Is there any way to get such behavior with standart destructuring-bind or any other standart macros? Or I have to use some ML-like pattern matching library, and if so - which one?

like image 346
coredump Avatar asked Aug 23 '14 15:08

coredump


2 Answers

It's not possible with DESTRUCTURING-BIND (you can't use a variable more than once, some compiler will complain). You can enumerate the variables, _1, _2, ... But then you have to ignore each of them.

LOOP can do it:

CL-USER 23 > (loop for ((a b nil c) nil d) in '(((1 2 3 4) 5 6)
                                                ((1 2 3 4) 5 6))
                   collect (list a b c d))
((1 2 4 6) (1 2 4 6))

NIL is used as the wildcard variable.

You can reuse the LOOP macro:

(defmacro match-bind (pattern object &body body)
  `(loop with ,pattern = ,object
         while nil
         finally (return (progn ,@body))))

CL-USER 37 > (match-bind ((a b nil c) nil d)
                 '((1 2 3 4) 5 6)
               (list a b c d))
(1 2 4 6)

You can use some LET-MATCH from some library. For example: https://github.com/schani/clickr/blob/master/let-match.lisp There are probably more fancy versions.

like image 175
Rainer Joswig Avatar answered Nov 12 '22 00:11

Rainer Joswig


There's nothing built into the language for this. Rainer Joswig's answer points out that loop can do some destructuring, but it doesn't do nearly as much. In an earlier version of this answer, I suggested traversing the destructuring lambda list and collecting a list of all the symbols that begin with _ and adding a declaration to the form to ignore those variables. A safer version replaces each one with a fresh variable (so that there are no repeated variables), and ignores them all. Thus something like

(destructuring-bind (_a (_b c)) object
  c)

would expand into

(destructuring-bind (#:g1 (#:g2 c)) object
  (declare (ignore #:g1 #:g2))
  c)

This approach will work OK if you're only using the "data-directed" described in 3.4.4.1.1 Data-directed Destructuring by Lambda Lists. However, if you're using "lambda-list-directed" approach described in 3.4.4.1.2 Lambda-list-directed Destructuring by Lambda Lists, where you can use lambda-list keywords like &optional, &key, etc., then things are much more complicated, because you shouldn't replace variables in some parts of those. For instance, if you have

&optional (_x '_default-x)

then it might be OK to replace _x with something, but not _default-x, because the latter isn't a pattern. But, in Lisp, code is data, so we can still write a macro that maps over the destructuring-lambda-list and replaces only in locations that are patterns. Here's somewhat hairy code that does just that. This takes a function and a destructuring lambda list, and calls the function for each pattern variable in the lambda list, along with the type of the argument (whole, required, optional, etc.).

(defun map-dll (fn list)
  (let ((result '())
        (orig list)
        (keywords '(&allow-other-keys &aux &body
                    &key &optional &rest &whole)))
    (labels ((save (x)
               (push x result))
             (handle (type parameter)
               (etypecase parameter
                 (list (map-dll fn parameter))
                 (symbol (funcall fn type parameter)))))
      (macrolet ((parse-keyword ((&rest symbols) &body body)
                   `(progn
                      (when (and (not (atom list))
                                 (member (first list) ',symbols))
                        (save (pop list))
                        ,@body)))
                 (doparameters ((var) &body body)
                   `(do () ((or (atom list) (member (first list) keywords)))
                      (save (let ((,var (pop list)))
                              ,@body)))))
        (parse-keyword (&whole)
          (save (handle :whole (pop list))))
        (doparameters (required)
         (handle :required required))
        (parse-keyword (&optional)
         (doparameters (opt)
          (if (symbolp opt)
              (handle :optional opt)
              (list* (handle :optional (first opt)) (rest opt)))))
        (when (and (atom list) (not (null list))) ; turn (... . REST) 
          (setq list (list '&rest list)))         ; into (... &rest REST)
        (parse-keyword (&rest &body)
         (save (handle :rest (pop list))))
        (parse-keyword (&key)
         (doparameters (key)
          (if (symbolp key)
              (handle :key key)
              (destructuring-bind (keyspec . more) key
                (if (symbolp keyspec)
                    (list* (handle :key keyspec) more)
                    (destructuring-bind (keyword var) keyspec
                      (list* (list keyword (handle :key var)) more)))))))
        (parse-keyword (&allow-other-keys))
        (parse-keyword (&aux)
         (doparameters (aux) aux))
        (unless (null list)
          (error "Bad destructuring lambda list: ~A." orig))
        (nreverse result)))))

Using this, it's pretty easy to write a destructuring-bind* that replaces each pattern variable beginning with _ with a fresh variable that will be ignored in the body.

(defmacro destructuring-bind* (lambda-list object &body body)
  (let* ((ignores '())
         (lambda-list (map-dll (lambda (type var)
                                 (declare (ignore type))
                                 (if (and (> (length (symbol-name var)) 0)
                                          (char= #\_ (char (symbol-name var) 0)))
                                     (let ((var (gensym)))
                                       (push var ignores)
                                       var)
                                     var))
                               lambda-list)))
    `(destructuring-bind ,lambda-list ,object
       (declare (ignore ,@(nreverse ignores)))
       ,@body)))

Now we should look at the expansions it produces:

(macroexpand-1
 '(destructuring-bind* (&whole (a _ . b)
                        c _ d
                        &optional e (f '_f)
                        &key g _h
                        &aux (_i '_j))
   object
   (list a b c d e f g)))
;=>                            
(DESTRUCTURING-BIND
    (&WHOLE (A #:G1041 &REST B) C #:G1042 D
     &OPTIONAL E (F '_F)
     &KEY G #:G1043
     &AUX (_I '_J))
    OBJECT
  (DECLARE (IGNORE #:G1041 #:G1042 #:G1043))
  (LIST A B C D E F G))

We haven't replaced anywhere we shouldn't (init forms, aux variables, etc.), but we've taken care of the places that we should. We can see this work in your example too:

(macroexpand-1
 '(destructuring-bind* ((_ (_ . title)
                         (_ . description)
                         _
                         (_ . video-id)))
   entry
   (list video-id title description)))
;=>
(DESTRUCTURING-BIND ((#:G1044 (#:G1045 &REST TITLE)
                              (#:G1046 &REST DESCRIPTION)
                              #:G1047
                              (#:G1048 &REST VIDEO-ID)))
    ENTRY
  (DECLARE (IGNORE #:G1044 #:G1045 #:G1046 #:G1047 #:G1048))
  (LIST VIDEO-ID TITLE DESCRIPTION))
like image 37
Joshua Taylor Avatar answered Nov 11 '22 22:11

Joshua Taylor