无损设置?

Nondestructive setf?

Common Lisp 似乎竭尽全力提供非破坏性函数(如 subst 和 remove)和破坏性函数以及修改宏(如 delete 和 rotatef)以供一般使用。据推测,这是为了有效地支持函数式和非函数式编程风格。但在无处不在的设计中似乎也特别偏向于非功能性风格 setfsetf 宏包含广义引用,显然足够灵活,可以修改任何可指定的位置(可能不可变的整数和字符除外)。尽管它具有 nonfunctional/destructive 行为,但这种能力可能是其广泛使用的原因。

问题是为什么没有相应的 "functional style" 非破坏性运算符,以 setf 为模式(称之为 put,因为 set 已经被采用),类似于其他 nondestructive/destructive lisp 运算符对。这样的运算符可能会采用相同的参数、位置和值,但会 return 嵌入位置的对象的副本,而不是该位置的新值。它还可能涉及某种通用复印机,标准 setf 在 return 打印之前简单地修改副本。然后可以使用非破坏性运算符代替 setf 进行大多数赋值,而 setf 保留用于非常大的对象。鉴于对通用复印机的(假定)要求以及从嵌入其中的任意位置恢复对象的需要,这样的运算符是否可行(甚至可能)?

Common Lisp 没有万能复印机,原因与它没有 CLOS objects (see, e.g., ): the power of MOP.

的内置 printable 表示相同。

具体来说,对象创建可能会产生难以保证复制的任意副作用。例如,定义 initialize-instance for your class to modify slots based something fluid (e.g., tides or just random). Then the result of make-instance called with the same arguments twice may be different. You might argue that this is an argument in favor of a memcpy 风格的通用复印机。但是,现在想象一个 class 执行实例记帐(一个 :allocation :class 槽,其中包含一个哈希 table 将唯一 ID 映射到实例)。现在,从复制的对象通过 table 的往返将得到一个 不同的 对象(原始对象,而不是副本)——这是一个重大的合同违规行为。而这些例子只是冰山一角。

但是,我不同意 Common Lisp 更鼓励命令式风格而不是函数式风格。它只是不鼓励你描述的风格(copy+setf)。这样想:从功能的 POV 来看,副本与原件没有区别(因为一切都是 immutable)。

也有 "small hints" 表现出对功能风格的明显偏好。 观察 "natural" 名称 (例如,append)被保留 对于 非破坏性 功能;破坏性版本 (例如,nconc) 晦涩难懂的名字。

此外,pathnames, characters and numbers (including composites like ratio and complex) 是 immutable,即所有路径名和数字函数创建新对象(函数式)。

因此,IMO,Common Lisp 鼓励程序员使用函数式风格,同时仍然使命令式风格可行,符合口号“简单的事情应该很容易,困难的事情应该是可能的".

另见 Kent Pitman's writeup

也没有通用的 setter,但是当你使用 SETF 时,你应该提供一个 DEFINE-SETF-EXPANDER. I suppose you could define the equivalent of lenses/functional references. For another approach, Clojure defines an update function (borrowed from other languages, I know it exists in Prolog), which is not universal either. The FSET 库提供了一些功能结构,特别是像 Clojure 中那样工作的关联映射。

暂时假设您将自己限制在结构上:

(defstruct foo a b c)
(defstruct bar x y z)

(defparameter *test*
  (make-foo :a (make-bar :x 0 :y 0 :z 0)
            :b 100
            :c "string"))

;; *test* is now
;; #S(FOO :A #S(BAR :X 0 :Y 0 :Z 0) :B 100 :C "string")

以下方法进行复制,它依赖于 copy-structureslot-value,虽然不标准,但为 well supported:

(defun update (structure keys value)
  (if (endp keys)
      value
      (destructuring-bind (key &rest keys) keys
        (let ((copy (copy-structure structure)))
          (setf (slot-value copy key)
                (update (slot-value copy key)
                         keys
                         value))
          copy))))

您可以按如下方式更新*test*

(update *test* '(a z) 10)

这里有痕迹:

0: (UPDATE #S(FOO :A #S(BAR :X 0 :Y 0 :Z 0) :B 100 :C "string") (A Z) 10)
  1: (UPDATE #S(BAR :X 0 :Y 0 :Z 0) (Z) 10)
    2: (UPDATE 0 NIL 10)
    2: UPDATE returned 10
  1: UPDATE returned #S(BAR :X 0 :Y 0 :Z 10)
0: UPDATE returned #S(FOO :A #S(BAR :X 0 :Y 0 :Z 10) :B 100 :C "string")

为了概括,您可以接受一个函数而不是一个值,这样您就可以通过使用 #'1+ 部分应用更新函数来实现 incf 的等价物(生成的闭包将接受 keys).

的列表

此外,您需要泛化 copy 操作,这可以通过泛型函数实现。同样,您可以使用其他类型的访问器,并将 slot-value 替换为通用 access 函数(其中有一个 (setf access) 操作)。但是,如果您想共享一些数据,这种通用 copy/setf 方法可能会造成浪费。例如,您只需要复制列表中指向您的数据的部分,而不需要复制它后面的 cons 单元格。

您可以定义一些工具来定义自定义更新程序:

(defmethod perform-update (new (list list) position)
  (nconc (subseq list 0 position)
         (list new)
         (nthcdr (1+ position) list)))