How to format a nested multiple-value-bind the let way?
Asked Answered
A

4

5

Recently, I've been often nesting several functions that return multiple values. However, unlike let, which me allows to write these calls elegantly into one big statement, I always end up with a lot of indentation.

My question is: having several multiple-valued functions such as

(defun return-1-and-2 ()
  (values 1 2))

(defun return-3-and-4 ()
  (values 3 4))

is it possible to achieve the same as

(multiple-value-bind (one two)
    (return-1-and-2)
  (multiple-value-bind (three four)
      (return-3-and-4)
    (list one two three four)))

but write it more concisely the let-way, i.e., something like

(multiple-let (((one two) (return-1-and-2))
               ((three four) (return-3-and-4)))
  (list one two three four))

?

Aborticide answered 1/2, 2020 at 18:54 Comment(0)
B
5

Probably there are similar constructs in libraries.

Note that it is more similar to let*, not let, since scope is nested.

One could write a macro. For example:

(defmacro multiple-value-let* ((&rest bindings) &body body)

  "Sets the scope for several ((var-0 ... var-n) form)
  binding clauses, using the multiple return values of the form."

  (if (null bindings)
      `(progn ,@body)
    (destructuring-bind (((&rest vars) form) &rest rest-bindings)
        bindings
      `(multiple-value-bind ,vars
           ,form
         (multiple-value-let* ,rest-bindings
           ,@body)))))

Example:

CL-USER 33 > (walker:walk-form
              '(multiple-value-let* (((one two)    (return-1-and-2))
                                     ((three four) (return-3-and-4)))
                 (list one two three four)))
(MULTIPLE-VALUE-BIND (ONE TWO)
    (RETURN-1-AND-2)
  (MULTIPLE-VALUE-BIND (THREE FOUR)
      (RETURN-3-AND-4)
    (PROGN (LIST ONE TWO THREE FOUR))))
Bora answered 1/2, 2020 at 19:30 Comment(1)
Thank you for the macro as well as for pointing out the let* analogy. Is it possible that you have a typo in the second listing, i.e., multiple-let* instead of multiple-value-let*?Offcolor
S
5

I have grown a bit fond of the library let-plus, which offers a let+ macro that has this option (among others):

(let+ (((&values one two) (return-1-and-2))
       ((&values three four) (return-3-and-4))
       (foo (bar))                  ; other examples
       (#(a b c) (some-vector)))    ;
  #| body… |#)
Suppositious answered 2/2, 2020 at 11:33 Comment(1)
Thank you for the tip, I did not know this library.Offcolor
E
5

In Serapeum, mvlet*:

Expand a series of nested multiple-value-bind forms.

  (mvlet* ((minutes seconds (truncate seconds 60))
           (hours minutes (truncate minutes 60))
           (days hours (truncate hours 24)))
    (declare ((integer 0 *) days hours minutes seconds))
    (fmt "~d day~:p, ~d hour~:p, ~d minute~:p, ~d second~:p"
         days hours minutes seconds))

https://github.com/ruricolist/serapeum/blob/master/REFERENCE.md#mvlet-rest-bindings-body-body

Exoskeleton answered 2/2, 2020 at 23:50 Comment(0)
H
1

I extended the above to allow let to handle not just multiple-value-bind but also labels. My code is a little simpler than serapeum's since they handle more cool cases than me. For example in serapeum's code, if a let+ has no special features, it expands to a normal let*. By ignoring those cool features, I get to write it all in just a dozen lines:

(defun %let+ (body xs)
  (labels ((fun (x) (and (listp x) (> (length x) 2)))
           (mvb (x) (and (listp x) (listp (car x)))))
    (if (null xs)
      body
      (let ((x (pop xs)))
        (cond
          ((fun x) `(labels ((,(pop x) ,(pop x) ,@x))       ,(%let+ body xs)))
          ((mvb x) `(multiple-value-bind ,(pop x) ,(pop x) ,@(%let+ body xs)))
          (t       `(let (,x)                          ,(%let+ body xs))))))))

(defmacro let+ (spec &rest body) (%let+ body spec))

In this let+ macro...

  • (let+ (x (y 1))... expands as normal
  • (let+ ((fn1 (arg1 arg1b) body1))... wraps fn1 in labels.
  • (let+ ((arg2a arg2b) body2))... does a multiple-value-bind on body2, binding its results to arg2a arg2b.

Example:

(defun fn2 (x y ) (values x (+ x y)))

(defun test-let+(&optional (x 1))
  (let+ (z                          ; normal let stuff
         (y 1)                      ; normal let stuff
         (z 2)                      ; normal let stuff
         (fn1 (x y) (+ x y))        ; define a local function
         ((a b) (fn2 x (fn1 y z)))) ; call multiple-value-bind
      (format t "~&a ~a b ~a x ~a y ~a z ~a~%" a b x y z)))

Which expands into this:

(DEFUN TEST-LET+ (&OPTIONAL (X 1))
  (LET (Z)
    (LET ((Y 1))
      (LET ((Z 2))
        (LABELS ((FN1 (X Y)
                   (+ X Y)))
          (MULTIPLE-VALUE-BIND (A B)
              (FN2 X (FN1 Y Z))
            (FORMAT T "a ~a b ~a x ~a y ~a z ~a~%" A B X Y Z)))))))

And runs like this....

> (test-let+) 

a 1 b 4 x 1 y 1 z 2
Halbert answered 10/2, 2022 at 3:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.