Две простые функции нажатия; один постоянно мутирует глобальную переменную, другой нет, почему?

Вот две простые функции, которые используют push для переданной переменной:

(defun push-rest (var) (push 99 (rest var)))

а также

(defun just-push (something) (push 5 something))

Первый навсегда изменит переданный var. Второй нет. Это довольно запутанно для тех, кто изучает поведение области видимости этого языка:

CL-USER> (defparameter something (list 1 2))    
SOMETHING
CL-USER> something
(1 2)
CL-USER> (just-push something)
(5 1 2)
CL-USER> something
(1 2)
CL-USER> (push-rest something)
(99 2)
CL-USER> something
(1 99 2)

Почему в push-rest область видимости переменной не является локальной для функции, как в just-push, когда они оба используют одну и ту же функцию, push?


person johnbakers    schedule 03.12.2013    source источник
comment
Не рекомендуется называть глобальную переменную something и иметь локальные переменные something. Используйте *something* в качестве имени глобальной переменной.   -  person Rainer Joswig    schedule 03.12.2013


Ответы (5)


Согласно практическому общему Лиспу Питера Сибеля, Глава 6. Переменные: Это может вам очень помочь :

Как и все переменные Common Lisp, параметры функций содержат ссылки на объекты. Таким образом, вы можете присвоить новое значение параметру функции в теле функции, и это не повлияет на привязки, созданные для другого вызова той же функции. Но если объект, переданный в функцию, является изменяемым, и вы изменяете его в функции, изменения будут видны вызывающему объекту, поскольку и вызывающий, и вызываемый будут ссылаться на один и тот же объект.

И сноска:

В терминах компилятора-писателя Common Lisp функции "передаются по значению". Однако передаваемые значения являются ссылками на объекты.

(Передача по значению также означает копирование, но мы не копируем объект, а копируем ссылку/указатель на объект.)

Как я отметил в другом комментарии:

Лисп не передает объекты. Lisp передает копии ссылок на объекты функциям. Или вы можете думать о них как об указателях. setf присваивает новый указатель, созданный функцией, чему-то другому. Предыдущий указатель/привязка не трогается. Но если вместо этого функция работает с этим указателем, а не устанавливает его, то она также работает с исходным объектом, на который указывает указатель. если вы парень C++, это может иметь для вас гораздо больше смысла.

person Jerome Baldridge    schedule 03.12.2013
comment
Если вы собираетесь копировать текст откуда-то еще, вам нужна цитата. Этот текст дословно скопирован из Practical Common Lisp, Глава 6. Переменные. - person Joshua Taylor; 03.12.2013
comment
@JoshuaTaylor Я напрямую связался с текстом в книге в своем ответе. - person Jerome Baldridge; 03.12.2013
comment
Извините, я не увидел ссылку (светло-голубой/серый на белом фоне рядом с черным текстом). Извини за это. Я бы все же предложил сделать его немного более заметным с помощью чего-то вроде «Согласно Practical Питера Сибеля. Common Lisp, Глава 6. Переменные: … - person Joshua Taylor; 03.12.2013

Вы не можете нажать переданную переменную. Lisp не передает переменные.

Лисп передает объекты.

Вы должны понимать оценку.

(just-push something)

Лисп видит, что just-push — это функция.

Теперь он оценивает something. Значение чего-либо — это список (1 2).

Затем он вызывает just-push с единственным аргументом (1 2).

just-push никогда не увидит переменную, ему все равно. Все, что он получает, это объекты.

(defun push-rest (some-list) (push 99 (rest some-list)))

Выше подталкивает 99 к остальным минусам переданного списка. Так как минусы видны снаружи, изменения видны снаружи.

(defun just-push (something) (push 5 something))

Выше помещает 5 в список, на который указывает something. Поскольку something не виден снаружи и никаких других изменений не было сделано, это изменение не видно снаружи.

person Rainer Joswig    schedule 03.12.2013
comment
По вашему объяснению, если something это просто объект, который снаружи не виден, то почему some-list тоже не является объектом, невидимым снаружи, а rest some-list является частью этого объекта, которая снаружи не видна? - person johnbakers; 03.12.2013
comment
@OpenLearner: something не является объектом. something — это переменная. some-list тоже не объект. some-list — это переменная. something указывает на список. some-list указывает на список. (push 99 (rest some-list)) изменяет список деструктивно. (push 5 something) изменяет локальную переменную, а не исходный список. - person Rainer Joswig; 03.12.2013
comment
Я думаю, что сбивающий с толку аспект такого поведения заключается в том, что не передача аргумента функции определяет, работаем ли мы с указателем на внешний объект или с привязкой к новому локальному объекту. Скорее, именно использование push делает это различие, что сбивает с толку. Я исхожу из такого языка, как C++, где либо вы передаете ссылку на объект, либо передаете по значению, и это определяется списком аргументов, а не тем, что вы на самом деле делаете внутри функции. - person johnbakers; 03.12.2013
comment
Не совсем. Лисп не передает объекты. Лисп передает копии ссылок на объекты функциям. Или вы можете думать о них как об указателях. setf присваивает новый указатель, созданный функцией, чему-то другому. Предыдущий указатель/привязка не трогается. Но если вместо этого функция работает с этим указателем, а не устанавливает его, то она также работает с исходным объектом, на который указывает указатель. если вы парень C++, это может иметь для вас гораздо больше смысла. - person Jerome Baldridge; 03.12.2013
comment
@OpenLearner: Это так же, как и во многих других языках. Если вы измените локальную переменную, она не будет видна снаружи. Если вы измените переданный объект, он может быть видим снаружи. В этом нет ничего особенного для Лиспа — это особенность всех языков, которые передают объекты как значения, а не как копии. Затем: PUSH — это макрос, который меняет место: он может изменять глобальные/локальные переменные, cons car/cdr, слоты объектов, записи хэш-таблицы и многое другое. Вы говорите PUSH, что нужно изменить, и он делает изменения. Разница: изменение локальной переменной по сравнению с изменением определенной cons-ячейки. - person Rainer Joswig; 03.12.2013

push работает иначе, когда ему передается символ или список в качестве второго аргумента. Возможно, вы поймете это лучше, если сделаете macroexpand на двух разных.

(macroexpand '(push 99 (rest var))) 
;;==>
(let* ((#:g5374 99)) 
  (let* ((#:temp-5373 var)) 
    (let* ((#:g5375 (rest #:temp-5373))) 
      (system::%rplacd #:temp-5373 (cons #:g5374 #:g5375)))))

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

(rplacd var (cons 99 (rest var)))

Теперь это изменяет cdr var таким образом, что каждая привязка к одному и тому же значению или спискам, имеющим один и тот же объект в своей структуре, изменяется. Теперь давайте попробуем другой:

(macroexpand '(push 5 something))
; ==>
(setq something (cons 5 something))

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

(defmacro just-push (lst)
  (if (symbolp lst)
      `(push 5 ,lst)
      (error "macro-argument-not-symbol")))

Это принимает только переменные в качестве аргумента и преобразует его в новый список, имеющий 5 в качестве первого элемента и исходный список в качестве хвоста. (just-push x) — это просто сокращение от (push 5 x).

Просто быть чистым. На диалекте алгола эквивалентный код будет выглядеть примерно так:

public class Node
{
  private int value;
  private Node next;

  public Node(int value, Node next)
  {
    this.value = value;
    this.next  = next;
  }

  public static void pushRest(Node var)
  {
    Node n   = new Node(99, var.next); // a new node with 99 and chained with the rest of var
    var.next = n; // argument gets mutated to have new node as next
  }

  public static void justPush(Node var)
  {
    var = new Node(5, var); // overwrite var
    System.out.print("var in justPush is: ");
    var.print();
  }

  public void print()
  {
    System.out.print(String.valueOf(value) + " ");
    if ( next == null )
      System.out.println();
    else
      next.print();
  }

  public static void main (String[] args)
  {
    Node n = new Node( 10, new Node(20, null)); 
    n.print();    // displays "10 20"
    pushRest(n);  // mutates list
    n.print();    // displays "10 99 20"
    justPush(n);  // displays "var in justPush is :5 10 99 20" 
    n.print();    // displays "10 99 20"
  }
}
person Sylwester    schedule 03.12.2013
comment
Я предполагаю, что для меня это вопрос, что именно представляет переменная в аргументе функции. У нас есть var, локальная переменная функции. Насколько я понимаю, это локальная привязка, созданная при запуске функции. То, что он по существу запускает rplacd, это нормально, но почему rplacd работает с объектом из внешней привязки, а не из локальной привязки? Новая переменная var создается при запуске функции; почему rplacd не работает с этой новой переменной, а не с предыдущей привязкой? - person johnbakers; 03.12.2013
comment
@OpenLearner В обоих случаях у вас есть локальная привязка, но поскольку объект для отправки представляет собой список, макрос использует rplacd вместо setq. rplacd изменяет фактический объект, а setq изменяет (локальную) привязку. - person Sylwester; 03.12.2013
comment
@OpenLearner Я не уверен, что вы привыкли к другим языкам, но я добавил пример того же поведения на Java. - person Sylwester; 03.12.2013
comment
Спасибо, но я на самом деле никогда не использовал Java. Я больше парень C++. - person johnbakers; 03.12.2013

(push item place)

Это работает следующим образом, когда форма используется для указания place, где это упоминается в setf:

(setf place (cons item place))
person BLUEPIXY    schedule 03.12.2013
comment
Да, но почему place виден снаружи в одной функции, но не виден в другой? - person johnbakers; 03.12.2013
comment
@OpenLearner не копирует содержимое переменных (например, список), которое передается функции. - person BLUEPIXY; 03.12.2013
comment
Например, это похоже на разницу типов значений и ссылочных типов, на которые ссылаются в других языках. - person BLUEPIXY; 03.12.2013

Судя по вашему профилю, вы знакомы с C-подобными языками. push — это макрос, и следующая эквивалентность примерно верна (за исключением того факта, что это приведет к тому, что x будет оцениваться дважды, а push — нет):

(push x y) === (setf x (list* x y))

Это почти макрос C. Рассмотрим аналогичный макрос incf (на самом деле CL определяет incf, но это не важно сейчас):

(incf x) === (setf x (+ 1 x))

В C, если вы делаете что-то вроде

void bar( int *xs ) {
  xs[0] = xs[0] + 1;   /* INCF( xs[0] ) */
}

void foo( int x ) {
  x = x + 1;           /* INCF( x ) */
}

и иметь звонки, как

bar(zs);               /* where zs[0] is 10 */
printf( "%d", zs[0] ); /* 11, not 10 */

foo(z);                /* where z is 10 */
printf( "%d", z );     /* 10, not 11 */

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

Иногда я задаюсь вопросом, не продвигают ли поклонники Лиспа (в том числе и я) идею о том, что Лисп отличается, настолько, что мы сбиваем людей с толку, заставляя их думать, что нет ничего одинакового.

person Joshua Taylor    schedule 03.12.2013