【问题标题】:Nondestructive setf?无损设置?
【发布时间】:2017-02-21 17:18:37
【问题描述】:

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

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

【问题讨论】:

    标签: functional-programming common-lisp setf


    【解决方案1】:

    Common Lisp 没有通用复制器,原因与它没有 CLOS objects 的内置可打印表示(参见,例如,Saving CLOS objects)的原因相同:MOP 的力量。

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

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

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

    此外,pathnamescharactersnumbers(包括像 ratiocomplex 这样的组合)是 不可变,即所有路径名和数字函数都会创建新对象(函数式)。

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

    另见Kent Pitman's writeup

    【讨论】:

    • 您似乎编写了上述 CLOS 对象读取器/写入器。 (谢谢!)是否有一个很好的例子来说明某些类型的可用对象的限制?
    • make-instance 添加一个after 方法,根据月相修改对象。那么你保存的对象就会和你读取的对象不一样了。
    【解决方案2】:

    也没有通用的二传手,但是对于 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")
    

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

    此外,您需要泛化 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)))
    

    【讨论】:

    • 那么,假设您将复制限制在基本的 lisp 对象(撇开 CLOS 实例),那么通用复制思想的本质是为每种类型的基本对象编写递归泛型方法吗?跨度>
    • @davypough 是的,默认的通用副本可以复制数据结构的所有组件(只有一层副本,上面显示的更新函数在需要时进行递归复制)。请注意,这将是循环引用的问题。
    猜你喜欢
    • 2014-03-28
    • 1970-01-01
    • 2018-03-10
    • 2011-03-11
    • 2011-01-17
    • 2021-12-14
    • 2011-12-20
    • 1970-01-01
    相关资源
    最近更新 更多