无损设定?

时间:2017-02-21 17:18:37

标签: functional-programming common-lisp setf

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

问题是为什么没有相应的"功能风格"非破坏性运算符,setf之后的模式(调用它,因为set已被采用),类似于其他非破坏性/破坏性的lisp运算符对。这样的运算符可能会使用相同的参数,一个位置和一个值,但会返回嵌入了该位置的对象的副本,而不是该位置的新值。它也可能涉及某种通用复印机,标准setf只需在返回之前修改副本。然后,对于大多数分配,可以使用非破坏性运算符代替setf,其中setf被保留用于非常大的对象。鉴于通用复印机的(推测)要求以及从嵌入其中的任意位置恢复物体的需要,这样的操作员是否可行(甚至可能)?

2 个答案:

答案 0 :(得分:6)

Common Lisp没有通用复印机,原因与它没有CLOS objects的内置可打印表示形式相同(参见,例如,Saving CLOS objects):MOP的强大功能。

具体而言,对象创建可能具有任意副作用,其复制难以保证。例如,为您的班级定义initialize-instance以根据流量(例如,潮汐或仅random)修改插槽。然后,使用相同参数两次调用make-instance的结果可能不同。您可能会认为这是支持memcpy式通用复印机的论据。但是,现在假设一个实例计算的类(一个:allocation :class槽,它包含一个将唯一ID映射到实例的哈希表)。现在,通过表格复制对象的往返将获得不同的对象(原始文件,而不是副本) - 违反主要合同。这些例子只是冰山一角。

然而,我不同意Common Lisp比功能风格更鼓励命令式风格。它只是劝阻你描述的风格(复制+ setf)。以这种方式思考:从功能POV开始,副本与原始版本无法区分(因为一切都是不可变的)。

还有“小提示”显示出对功能风格的明显偏好。 观察“自然”的名字 (例如,append)被保留 用于非破坏性功能;破坏性的版本 (例如,nconc) 晦涩的名字。

此外,pathnamescharactersnumbers(包括ratiocomplex等复合词) immutable ,即所有路径名和数字函数都创建新对象(功能样式)。

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

另见Kent Pitman's writeup

答案 1 :(得分:2)

也没有通用的setter,但是使用SETF时,你应该在使用DEFINE-SETF-EXPANDER时提供一个。我想你可以定义lenses/functional references的等价物。对于另一种方法,Clojure定义了一个update函数(借用其他语言,我知道它存在于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-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")

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

此外,您需要概括 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)))