Условное связывание переменных в Common Lisp

Я хочу выполнить функцию с 2 локальными переменными, но значения этих переменных должны зависеть от некоторых условий. Например, допустим, у меня есть 2 переменные x а также yи я хочу обменять их внутри let если y > x, Обмен должен быть временным, я не хочу изменять состояние с rotatef, Мой код будет выглядеть примерно так:

(setq x 2)
(setq y 1)
(let (if (> x y) ((x y) (y x)) ((x x) (y y)))
  (cons x y)) ; should return (1 . 2)

Но выражение внутри let не действует Лисп. Как условно назначить значения локальным переменным? Работа заключается в том, чтобы поместить тело в flet и назвать его с другими аргументами, но это выглядит неуклюже:

(flet ((body (x y) (cons x y)))
  (if (< x y)
      (body x y)
      (body y x)))

6 ответов

Решение

Связывание нескольких значений и значений

Есть много альтернатив, некоторые из которых уже были указаны в других ответах. Я думаю, что вопрос в заголовке ("Условное связывание переменных в Common Lisp") является хорошим примером для привязки нескольких значений и значений. Далее я использовал разные имена переменных, чтобы было ясно, где x и y и откуда берутся исходные значения. Хотя имена могут быть одинаковыми; это просто затмевает их внутри.

(let ((a 3)
      (b 2))
  (multiple-value-bind (x y)
      (if (< a b)
          (values a b)
          (values b a))
    (cons x y)))
;=> (2 . 3)

Затем, используя немного макрологии, мы можем сделать это немного чище, как это делал coredump:

(defmacro if-let (test bindings &body body)
  "* Syntax:
let ({var | (var [then-form [else-form]])}*) declaration* form* => result*
* Description: 
Similar to LET, but each binding instead of an init-form can have a
then-form and and else-form.  Both are optional, and default to NIL.
The test is evaluated, then variables are bound to the results of the
then-forms or the else-forms, as by LET."
  (let ((bindings (mapcar #'(lambda (binding)
                              (destructuring-bind (variable &optional then else)
                                  (if (listp binding) binding (list binding))
                                (list variable then else)))
                          bindings)))
    `(multiple-value-bind ,(mapcar 'first bindings)
         (if ,test
             (values ,@(mapcar 'second bindings))
             (values ,@(mapcar 'third bindings)))
       ,@body)))

(pprint (macroexpand-1 '(if-let (< x y) ((x x y)
                                         (y y x))
                         (cons x y))))

; (MULTIPLE-VALUE-BIND (X Y)
;     (IF (< X Y)
;         (VALUES X Y)
;         (VALUES Y X))
;   (CONS X Y))

(let ((a 3) (b 2))
  (if-let (< a b)
      ((x a b)
       (y b a))
    (cons x y)))
;=> (2 . 3)

Сравнение с прогв

С точки зрения использования, у этого есть некоторое сходство с ответом синдиката, но связывание с несколькими значениями устанавливает привязки точно так же, как let делает: лексический по умолчанию, но глобальное или локальное специальное объявление сделает привязки динамическими. С другой стороны, progv устанавливает динамические привязки. Это означает, что если привязки полностью введены progv, вы не увидите большой разницы (кроме попыток вернуть замыкания), но вы не сможете скрыть привязки. Мы можем видеть это без всякой условной работы. Вот два примера фрагментов. Во-первых, мы видим, что внутренняя ссылка на x на самом деле относится к лексической привязке, а не к динамической привязке, установленной progv. Чтобы сослаться на тот, который установлен progv, вам нужно объявить внутреннюю ссылку особенной. progv не принимает декларации, но мы можем использовать их локально.

(let ((x 1))
  (progv '(x) '(2)
    x))
;=> 1

(let ((x 1))
  (progv '(x) '(2)
    (locally (declare (special x))
      x)))
;=> 2

Аргумент "несколько значений" на самом деле выполняет связывание так, как мы ожидаем:

(let ((x 1))
  (multiple-value-bind (x) (values 2)
    x))
;=> 2

Вероятно, лучше использовать конструкцию связывания, такую ​​как множественное связывание, которая по умолчанию устанавливает лексические привязки, как это делает let.

Если вы не хотите использовать progvКак уже упоминалось, Синдикат, вы всегда можете сделать что-то подобное:

(defmacro let-if (if-condition then-bindings else-bindings &body body)
  `(if ,if-condition
     (let ,then-bindings
       ,@body)
     (let ,else-bindings
       ,@body)))

Так выражение как

(let-if (> x y) ((x y) (y x)) ((x x) (y y))
       (cons x y))

Будет расширяться в:

(IF (> X Y)
(LET ((X Y) (Y X))
  (CONS X Y))
(LET ((X X) (Y Y))
  (CONS X Y)))

rotatef

Как насчет:

CL-USER> (defvar x 2)
X
CL-USER> (defvar y 1)
Y
CL-USER> (let ((x x)    ; these variables shadow previously defined
               (y y))   ; X and Y in body of LET
           (when (> x y)
             (rotatef x y))
           (cons x y))
(1 . 2)
CL-USER> x              ; here the original variables are intact
2                       ; ^
CL-USER> y              ; ^
1                       ; ^

Тем не менее, я думаю, что в каждом таком практическом случае есть более изощренные способы решения проблемы без макросов. Ответ от msandiford, пожалуй, лучший с функциональной точки зрения.

psetf

Хотя rotatef действительно эффективный метод (вероятно, он будет скомпилирован примерно с тремя машинными инструкциями, меняющими указатели в памяти), он не является общим.

Вскоре после публикации вопроса Райнер Йосвинг опубликовал просто отличное решение в качестве комментария. К своему стыду, я проверил макрос psetf только несколько минут назад, и это должно быть очень эффективным и общим решением.

макрос psetf сначала оценивает его четные аргументы, затем присваивает оцененные значения переменным в нечетных позициях, как setf делает.

Итак, мы можем написать:

(let ((x x)
      (y y))
  (when (> x y)
    (psetf x y y x))
  ...)

И это все, можно связать что угодно с чем угодно. Я думаю, что это намного лучше, чем использовать макросы. Так как:

  • Я не думаю, что это такая распространенная ситуация;
  • Некоторые макросы в опубликованных ответах повторяют код своего тела, который может быть очень большим: таким образом, вы получаете скомпилированный файл большего размера (это справедливая цена за использование макроса, но не в этом случае);
  • Каждый пользовательский макрос усложняет понимание кода для других людей.

Одним из решений является использование progv вместо let, его первый аргумент - это список символов для привязки значений, второй аргумент - это список значений, остальные - это тело.

(progv '(x y) (if (< x y) (list x y) (list y x))
  (cons x y)) ; outputs (1 . 2)

Другой альтернативой может быть:

(let ((x (min x y))
      (y (max x y)))
  (cons x y))

Мое предложение будет одним из destructuring-bind или же multiple-value-bind,

Если вы ожидаете, что вам придется много делать, я бы предложил использовать макрос для генерации привязок. Я предоставил возможный макрос (не проверен).

(defmacro cond-let (test-expr var-bindings &body body)
  "Execute BODY with the VAR-BINDINGS in place, with the bound values depending on 
   the trueness of TEST-EXPR.

   VAR-BINDINGS is a list of (<var> <true-value> <false-value>) with missing values 
   being replaced by NIL."

  (let ((var-list (mapcar #'car var-bindings))
        (then-values (mapcar #'(lambda (l)
                                 (when (cdr l) 
                                   (nth 1 l)))
                             var-bindings))
        (else-values (mapcar #'(lambda (l)
                                 (when (cddr l))
                                    (nth 2 l)))
                             var-bindings))
     `(destructuring-bind ,var-list
         (if ,test-expr
             (list ,@then-values)
           (list ,@else-values)))))
Другие вопросы по тегам