порядок имеет значение с возвратом в SBCL - PullRequest
0 голосов
/ 03 декабря 2018

В третий день я пытаюсь изучить Common Lisp (SBCL), выполняя Advent of Code .Я понимаю, что существует более одного типа return.Мне интересно, может ли кто-нибудь объяснить мне, почему следующая функция вернет nil (это имеет смысл)

(defun fn (n)
    (cond ((zerop n) 
           (return-from fn nil))
          (t  
           (write-line "Hello World") 
           (fn (- n 1)))))

, но следующая функция вернет "Hello World" (это не имеет смысла длямне).

(defun fn (n)
    (cond ((zerop n) 
           (return-from fn nil))
          (t 
           (fn (- n 1)) 
           (write-line "Hello World"))))

Я нашел отличный пост, освещающий некоторые аспекты return поведения SBCL здесь здесь , но, насколько я понимаю, он не затрагивает эту конкретную деталь.

РЕДАКТИРОВАТЬ: loop вызов является более разумным способом написания этой функции, но это не тот способ, которым я обнаружил это поведение.Я подозреваю, что такое поведение возникает потому, что fn вызывается рекурсивно.

Ответы [ 3 ]

0 голосов
/ 03 декабря 2018

(Я начал писать это до ответа Сильвестра, что, как мне кажется, в основном лучше.)

Критическое различие между языками семейства Лисп и многими другими языками состоит в том, что языки семейства Лисп являются «языками выражения».Технически это означает, что в языках, таких как (скажем) C или Python, существует два вида конструкций:

  • выражения, которые имеют значения;
  • выражения, которые не имеют;

В то время как в языках семейства Lisp есть одна вещь: выражения, которые имеют значения.В результате этого языки семейства Lisp иногда называют «языками выражений».

Это имеет огромное значение, если вы хотите писать функции, которые являются вещами, которые возвращают значения (вызов функции - это выражение в другомслова).

Обычные языки (например, Python)

В языке, который не является языком выражений, тогда, если вы определяете функцию и попадаете в середину некоторой конструкции, котораяявляется оператором, и вы хотите вернуть значение, вы должны использовать некоторую специальную магическую конструкцию, часто называемую return, чтобы сделать это.Таким образом, в Python, где условные выражения являются операторами, вы можете написать:

def fib(n):
    if n < 2:
        return n
    else:
        return fib(n - 1) + fib(n - 2)

И на самом деле у вас есть для использования return, поскольку тело определения функции в Python - это последовательностьоператоров, поэтому для возврата любого вида значения вам нужно использовать return.

На самом деле, Python (и C, и Java & c & c) имеют специальную форму условного выражения, которое является выражением: в Python это выглядит так:

def fib(n):
    return n if n < 2 else (fib(n - 1) + fib(n - 2)

По-другому выглядит в C, но делает то же самое.

Но вам все еще нужно это надоедливое return (ОК, только один изих сейчас), и это обнаруживает еще одну особенность таких языков: если какое-то место в синтаксисе требует оператора, вам, как правило, нужно иметь оператор там, или если вы можете поместить выражение там, его значение просто отбрасывается.Таким образом, вы можете попробовать что-то вроде этого:

def fib(n):
    n if n < 2 else (fib(n - 1) + fib(n - 2)

И это синтаксически нормально - выражение превращается в оператор - но оно терпит неудачу во время выполнения, потому что функция больше не возвращает полезное значение.В Python вы можете обойти это, если хотите, чтобы люди вас ненавидели:

fib = lambda n: n if n < 2 else fib(n - 1) + fib(n - 2)

Люди Python будут ненавидеть вас, если вы сделаете это, и это также бесполезно, потому что Python только для lambda 1041 * принимает выражения, поэтому то, что вы можете написать, ограничено.

Lisp

Lisp не имеет ничего из этого: в Lisp все является выражением и, следовательно, все имеетзначение , вам просто нужно знать, откуда оно.Еще есть return (в CL, во всяком случае), но вам нужно использовать его гораздо реже.

Но, конечно, люди часто хотят писать программы, которые выглядят так: «Сделай это, затем сделай это,затем сделайте это », где большая часть работы выполняется для побочного эффекта, поэтому Лиспс обычно имеет какую-то конструкцию последовательности, которая позволяет вам просто иметь кучу выражений одно за другим, все, кроме (обычно) одного из которыхполучить оценку на побочный эффект.В CL самая распространенная секвенирующая конструкция называется progn (по историческим причинам).(progn ...) является выражением, составленным из других выражений, а его значением является значение последнего выражения в его теле.

progn настолько полезно, что группа других конструкций имеет «неявное progn»в них.Два примера - это определения функций (тело defun является неявным progn) и cond (тело cond -класса является неявным `progn).

Ваша функция

Вот ваша функция (первая версия) с указанием различных ее частей

(defun fn (n)
  ;; the body of fn is an implicit progn with one expression, so
  ;; do this and return its value
  (cond
   ;; the value of cond is the value of the selected clause, or nil
   ((zerop n)
    ;; the body of this cond clause is an implicit progn with on
    ;; expression so do this and ... it never returns
    (return-from fn nil))
   (t
    ;; the body of this cond clause is an implicit progn with two expressions, so
    ;; do this for side-effect
    (write-line "Hello World")
    ;; then do this and return its value
    (fn (- n 1)))))

Вот вторая версия

(defun fn (n)
  ;; the body of fn is an implicit progn with one expression, so
  ;; do this and return its value
  (cond
   ;; the value of cond is the value of the selected clause, or nil
   ((zerop n)
    ;; the body of this cond clause is an implicit progn with on
    ;; expression so do this and ... it never returns
    (return-from fn nil))
   (t
    ;; the body of this cond clause is an implicit progn with two expressions, so
    ;; do this for side-effect
    (fn (- n 1))
    ;; then do this and return its value
    (write-line "Hello World"))))

Итак, вы можете видеть, что здесь происходит: в первой версии возвращаемое значение равно либо nil, либо значению рекурсивного вызова (также nil).Во второй версии возвращаемое значение равно либо nil, либо тому, что возвращает write-line.И получается, что write-line возвращает значение его аргумента, так что это то, что вы получаете, если вы называете его с целым числом больше нуля.

Почему в Lisp есть return-from вообще?

Одна вещь, которая должна быть сразу понятна из всего этого языка выражений, заключается в том, что вам вряд ли когда-нибудь понадобится явно что-то возвращать в Лиспе: у вас просто есть выражение, которое вычисляет желаемое значение.Но есть два хороших использования (которые, возможно, на самом деле одинаковые) явных возвратов.

Во-первых, иногда вы делаете какой-то большой поиск чего-либо в виде группы вложенных циклов и в некоторыхто, что вы просто хотите сказать: «Хорошо, нашел это, вот ответ».Вы можете сделать это одним из двух способов: вы можете аккуратно структурировать свои циклы так, чтобы, как только вы нашли то, что вы хотите, они все хорошо заканчивались и значение возвращалось обратно, или вы можете просто сказать «вот ответ».Последняя вещь - то, что делает return-from: она просто говорит: «Я закончил, осторожно размотайте стек и верните это»:

(defun big-complicated-search (l m n)
  (dotimes (i l)
    (dotimes (j m)
      (dotimes (k n)
        (let ((it (something-involving i j k l m n)))
          (when (interesting-p it)
            (return-from big-complicated-search it)))))))

И return-from делает это в правомway :

(defun big-complicated-file-search (file1 file2)
  (with-open-file (f1 file1)
    (with-open-file (f2 file2)
      ...
      (when ...
        (return-from big-complicated-search found)))))

Когда вы вызываете это, и когда вещь обнаружена, return-from будет гарантировать, что два файла, которые вы открыли, правильно закрыты.

во-вторых, это почти то же самое, что иногда нужно просто сдаться, и return-from - хороший способ сделать это: он сразу возвращается, занимается очисткой (см. выше) и, как правило, неплохоспособ сказать «хорошо, я сдаюсь сейчас».На первый взгляд это похоже на то, что вы бы сделали с какой-то системой обработки исключений, но на самом деле есть два критических различия:

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

Наконец, механизм исключительного возврата через обработку ошибок просто ужасен.

0 голосов
/ 04 декабря 2018

Ваш код:

(defun fn (n)
  (cond ((zerop n) (return-from fn nil))
    (t (write-line "Hello World") (fn (- n 1)))
    )
  )

В приведенном выше коде несколько ошибочных вещей:

(defun fn (n)
  (cond ((zerop n) (return-from fn nil))         ; 1) the return from is not needed
    (t (write-line "Hello World") (fn (- n 1)))  ; 2) this line is not correctly
                                                 ;    indented
    )                                            ; 3) dangling parentheses Don't. Never.
                                                 ;    also: incorrect indentation 
  )
  1. первое предложение cond уже возвращает значение,просто напишите nil в качестве возвращаемого значения.Тогда все cond возвращает это значение.Очень редко вам нужно return или return-from из предложения cond.
  2. использовать редактор для отступа вашего кода.В GNU Emacs / SLIME команда control - meta - q сделает отступ выражения.Для получения справки о командах редактора в текущем режиме см .: control - h m для справки mode .
  3. сделайте отступ правильно и не используйте висячие скобки.Они бесполезны в Лиспе.Научитесь использовать редактор для правильного отступа кода - это гораздо полезнее, чем помещать неправильно заключенные в круглые скобки отдельные строки. tab отступ для текущей строки.

Для новичка удобнее форматировать код следующим образом:

(defun fn (n)
  (cond ((zerop n)
         (return-from fn nil))
        (t
         (write-line "Hello World")
         (fn (- n 1)))))

Код тогда будет выглядеть какДерево префиксов.

Также не забудьте отключить вставку вкладок в GNU Emacs. Поместите это в файл инициализации emacs: (setq-default indent-tabs-mode nil).Вы можете вычислять выражения Emacs Lisp также на лету с помощью meta> - : .

Теперь согласно 1. приведенный выше код обычно записывается как:

(defun fn (n)
  (cond ((zerop n)
         nil)
        (t
         (write-line "Hello World")
         (fn (- n 1)))))

Когда n равен нулю, выбирается первое предложение и возвращается его последнее значение.Другие пункты не рассматриваются -> cond возвращает nil -> функция fn возвращает nil.

Обычно я бы написал выше рекурсивную функцию, например:

(defun fn (n)
  (unless (zerop n)
    (write-line "Hello World")
    (fn (- n 1))))

unless возвращает nil, если (zerop n) истинно.Другой вариант:

(defun fn (n)
  (when (plusp n)
    (write-line "Hello World")
    (fn (- n 1))))

Вы МОЖЕТЕ использовать return-from, но в случае, если неясно: вам это не нужно большую часть времени.

0 голосов
/ 03 декабря 2018

В отличие от семейства языков С, в Лиспе есть все выражения.Это означает, что вы «возвращаете» результат выражения.например.

(+ (if (< x 0) 
       (- x) 
       x) 
    3)

Здесь результат if состоит в том, что это будет абсолютное значение x.Таким образом, если x равно -5 или 5, результатом выражения будет 8.Вы можете написать abs следующим образом:

(defun my-abs (v)
  (if (< v 0)
      (- v)
      v))

Обратите внимание, я не использую return.Результат if является последним выражением, и это означает, что результат этого является результатом my-abs.

Ваши две функции могут быть написаны так:

(defun fn1 (n)
  (cond 
    ((zerop n) nil)
    (t (write-line "Hello World") (fn1 (- n 1)))))

И

(defun fn2 (n)
  (cond 
    ((zerop n) nil)
    (t (fn2 (- n 1)) (write-line "Hello World"))))

Излишне говорить, что (write-line "Hello World") возвращает свой аргумент в дополнение к выводу аргумента.Таким образом, всякий раз, когда это последнее выражение, оно будет результатом.Для каждого n выше 0 он будет выполнять рекурсию первым, а каждый конец, кроме первого, будет возвращать "Hello World".Если вы позвоните (fn2 0), результат будет nil, такой же, как fn1.

РЕДАКТИРОВАТЬ

Кто-то может спросить, какова цель return и return-from, когда для этого явно мало пользы.Если вам нужно что-то, отличное от результата по умолчанию, в макросе loop, то обычно это делается с помощью предложения finally.

(defun split-by (test list &key (return-form #'values))
  "Split a list in two groups based on test"
  (loop :for e :in list
        :if (funcall test e) 
          :collect e :into alist
        :else 
          :collect e :into blist
        :finally (return (funcall return-form alist blist))))

(split-by #'oddp '(1 2 3 4) :return-form #'list)
; ==> ((1 3) (2 4))

Другой способ - если вы делаете рекурсию и хотите отменить все, когда вы знаете результат, который вы можете использовать return-from:

(defun find-tree-p (needle haystack &key (test #'eql))
  "search the tree for element using :test as comparison"      
  (labels ((helper (tree)
             (cond ((funcall test tree needle) 
                    (return-from find-tree t))
                   ((consp tree) 
                    (helper (car tree)) 
                    (helper (cdr tree)))
                   (t nil))))
   (helper haystack)))


(find-tree '(f g) '(a b c (d e (f g) q) 1 2 3) :test #'equal)
; ==> (f g) ; t

Теперь, если вы этого не сделалиreturn-from у вас была бы логика проверить возвращаемое значение, чтобы увидеть, нужно вам продолжить или нет.Если вы хотите обработать элементы и не хотите дважды проходить проверку достоверности перед вычислением результата, вы можете просто начать вычисления и использовать return-from в качестве call/cc.Эту функцию можно использовать для отображения списков списков, и она останавливается на самом коротком списке, поэтому она должна становиться (), когда первый подсписок пуст:

(defun cdrs (lists)
  "return the cdrs if all elements are cons, () otherwise"      
  (loop :for list :in lists
        :when (null list) :do (return-from cdrs '())
        :collect (cdr list)))

(cdrs '((a) (b) (c))) ; ==> (nil nil nil)
(cdrs '((a) (b) ()))  ; ==> ()
...