1

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

问题是为什么没有相应的“功能风格”非破坏性操作符,仿照其他非破坏性/破坏性 lisp 操作符对setf(称之为 put,因为set已经采用)。这样的运算符可能会采用相同的参数、位置和值,但会返回嵌入该位置的对象的副本,而不是该位置的新值。它也可能涉及某种通用复印机,标准setf只是在返回之前修改副本。然后可以使用非破坏性运算符代替setf大多数分配,其中setf为非常大的对象保留。考虑到对通用复印机的(假定)要求以及从嵌入其中的任意位置恢复对象的需要,这样的操作员是否可行(甚至可能)?

4

2 回答 2

6

Common Lisp 没有通用复制器,原因与它没有内置的CLOS 对象可打印表示(参见,例如,保存 CLOS 对象)相同:MOP的强大功能。

具体来说,对象创建可能会产生难以保证复制的任意副作用。例如,initialize-instance为您的类定义以修改基于流体的槽(例如,潮汐或只是random)。那么两次make-instance调用相同参数的结果可能会有所不同memcpy您可能会争辩说这是支持-style 通用复制器的论据。但是,现在想象一个执行实例记帐的类(一个:allocation :class包含将唯一 ID 映射到实例的哈希表的槽)。现在,从复制对象通过表的往返将获得不同的对象(原始对象,而不是副本) - 严重违反合同。而这些例子只是冰山一角。

但是,我不同意 Common Lisp 鼓励命令式风格而不是函数式风格。它只是不鼓励您描述的样式(copy+ setf)。可以这样想:从功能 POV 来看,副本与原件无法区分(因为一切都是不可变的)。

还有“小提示”显示出对功能风格的明显偏好。请注意,“自然”名称(例如,append)是为非破坏性功能保留的;破坏性版本(例如,nconc)被赋予晦涩的名称。

此外,路径名字符数字(包括和之类的组合ratiocomplex不可变的,即所有路径名和数字函数都会创建新对象(函数式风格)。

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

另见Kent Pitman 的文章

于 2017-02-21T18:23:06.813 回答
2

也没有通用设置器,但是使用 SETF 时,您应该在使用DEFINE-SETF-EXPANDER时提供一个。我想你可以定义等效的lens/functional references。对于另一种方法,Clojure 定义了一个更新函数(借用其他语言,我知道它存在于 Prolog 中),它也不是通用的。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-structureand slot-value,虽然不是标准的,但得到了很好的支持

(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")

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

此外,您需要泛化复制操作,这可以通过泛型函数实现。同样,您可以使用其他类型的访问器,并替换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)))
于 2017-02-21T19:56:33.847 回答