Common Lisp似乎竭尽全力提供非破坏性功能(如subst& remove)和破坏性功能,并修改宏(如delete& rotatef)以供一般使用。据推测,这是为了有效地支持功能和非功能的编程风格。但是在无处不在的setf
的设计中似乎也存在对非功能性风格的特殊偏见。包含通用引用的setf
宏显然足够灵活,可以修改任何可指定的位置(可能除了不可变的整数和字符外)。尽管它没有功能/破坏性行为,但这种力量可能是其广泛使用的原因。
问题是为什么没有相应的"功能风格"非破坏性运算符,setf
之后的模式(调用它,因为set
已被采用),类似于其他非破坏性/破坏性的lisp运算符对。这样的运算符可能会使用相同的参数,一个位置和一个值,但会返回嵌入了该位置的对象的副本,而不是该位置的新值。它也可能涉及某种通用复印机,标准setf
只需在返回之前修改副本。然后,对于大多数分配,可以使用非破坏性运算符代替setf
,其中setf
被保留用于非常大的对象。鉴于通用复印机的(推测)要求以及从嵌入其中的任意位置恢复物体的需要,这样的操作员是否可行(甚至可能)?
答案 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
)
晦涩的名字。
此外,pathnames,characters和numbers(包括ratio
和complex
等复合词)
immutable ,即所有路径名和数字函数都创建新对象(功能样式)。
因此,IMO,Common Lisp鼓励程序员使用功能风格,同时仍然使命令式风格可行,符合口号“简单的事情应该是容易的,硬件应该是可能的”。
答案 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-structure
和slot-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)))