O comportamento de setf
quando combinado com let
é confuso para mim. Aqui, setf
não está alterando o segundo valor da minha lista.
(defun to-temp-field (lst)
(let ((old-value (nth 2 lst))
(new-value 'hello))
(progn
(setf old-value new-value)
lst)))
Mas se eu fizer sem let
, muda ok:
(defun to-temp-field (lst)
(let ((new-value 'hello))
(progn
(setf (nth 2 lst) new-value)
lst)))
O que está causando esse comportamento?
Common Lisp é uma das primeiras linguagens a exibir call-by-sharing , um paradigma que agora é extremamente comum em linguagens de programação de tipo dinâmico e de nível superior. Em uma linguagem de chamada por compartilhamento, os valores vinculados a uma variável passam implicitamente por uma espécie de cópia superficial, pegando uma nova versão do ponteiro ou dos dados superiores, mas deixando quaisquer dados aninhados idênticos.
Em um Lisp, essa distinção é realmente mais fácil de ver. A célula cons de nível superior é copiada, mas quaisquer ponteiros aninhados são deixados de lado.
Aqui, não
old-value
há referência a uma posição na lista. É uma cópia superficial do valor na posição 2 da lista. Conforme indicado na resposta de khachik, na verdade é apenas uma macro muito complicada, e as macros despacham seu comportamento de acordo com a sintaxe que veem. Então, o que é visto como seu primeiro argumento é . Isso está acontecendo em tempo de compilação, então não tem como saber se veio de uma lista, ou de uma chamada, então basta definir a variável local (usando basicamente ).setf
setf
old-value
setf
old-value
nth
setq
Agora, considere seu segundo exemplo,
Aqui,
setf
vê(nth 2 lst)
como seu primeiro argumento. Esta não é uma variável local. Na verdade, esta é uma expressão semelhante a uma chamada de função. E como podemos ver no hyperspec ,nth
na verdade tem duas entradas: uma para uma chamada de função normal e outra para quando é usada emsetf
. Isso porque, assim como podemos definir funções normais comoPodemos definir separadamente um
setf
acessador comoIsso não é pseudocódigo.
defun
na verdade, aceita(setf whatever)
como um nome de função declarado válido e, quando você usasetf
em uma chamada de função, a macro procura uma função com esse nome.setf
pega um lugar e um valor, ou uma série de pares de lugar/valor, e altera o valor do lugar para o novo valor.No primeiro exemplo de código
old-value
há uma variável lexical vinculada aolet
formulário. Variáveis lexicais são lugares , então chamar(setf old-value new-value)
define o valor deold-value
tonew-value
.Os formulários de função também podem ser locais e
nth
é uma função que pode especificar um local. No segundo exemplo de código, a chamada(setf (nth 2 lst) new-value)
define o valor do local(nth 2 lst)
como valornew-value
.Assim no primeiro exemplo o place
old-value
, uma variável lexical, é atualizado, enquanto no segundo exemplo o place(nth 2 lst)
, um elemento da listalst
é atualizado.Algumas coisas a serem observadas:
O segundo exemplo atualiza o terceiro elemento,
lst
uma vez que as listas são indexadas em zero no Common Lisp.Não há necessidade do
progn
formulário dentro delet
, pois o corpo de umlet
formulário é implícitoprogn
.A tentativa de modificar um literal causa um comportamento indefinido no Common Lisp, então você não deve chamar a segunda função com uma lista entre aspas como esta:
(to-temp-field '(1 2 3))
ou com uma variável vinculada a uma lista literal, mas sim assim:(to-temp-field (list 1 2 3))
ou com uma variável vinculada a um lista que foi recentemente construída.setf
é uma macro que, simplesmente, entende o que está sendo definido e chama o conjunto correspondente com base nisso, então(setf old-value new value)
simplesmente define o valor, enquanto no segundo caso ele se comporta conforme o esperado.setf
é uma macro e faz coisas completamente diferentes quando o primeiro argumento é um símbolo do que algum acessador para uma estrutura de lista.É muito fácil ver que quando você fornece um símbolo, ele faz algo completamente diferente do que se você fornecesse o
nth
formulário:A última linha que você vê chama internal function
%setnth
. Isso é específico da implementação, então o SBCL faz outra coisa. No entanto; Isso claramente busca o local e faz o mesmo que(rplaca (cddr lst) new-value)
quando você armazenou em cache o valor de(nth 2 lst)
toold-value
:Isso
setq
apenas religará a variável localold-value
para apontar para99
. Você não pode armazenar em cache o local antigo, pois a macro não o compreenderá mais.