CLtL2参考清楚地区分了非破坏性和破坏性的普通lisp操作。但是,在破坏性阵营中,在标记简单返回结果的那些与另外修改一个地方(作为参数)以包含结果的那些之间似乎不太清楚。通常的兼并" f"这样的地方修改操作(例如,setf,incf,alexandria:deletef)有点零星,并且也适用于许多地方访问者(例如,aref,getf)。在理想的函数式编程风格中(仅基于返回值),这种混淆可能不是问题,但似乎它可能导致在一些使用场所修改的实际应用中出现编程错误。由于不同的实现可以不同地处理位置结果,因此不会影响可移植性?甚至似乎很难测试特定实现的方法。
为了更好地理解上述区别,我将破坏性的common-lisp 序列操作划分为两个类别,对应于"参数返回"和"操作返回"。有人可以为我验证或使这些类别无效吗?我假设这些类别也适用于其他类型的破坏性操作(对于列表,散列表,数组,数字等)。
参数返回:填充,替换,映射到
操作返回:delete,delete-if,delete-if-not,delete-duplicates,nsubstitute,nsubstitute-if,nsubstitute-not-if,nreverse,sort,stable-sort,merge
答案 0 :(得分:1)
但是,在破坏性的阵营中,在标记仅仅返回结果的那些之间的区别似乎不那么明确。
尽管存在诸如n
前缀之类的有用约定,但关于哪种操作具有破坏性或不具有破坏性,没有简单的句法标记。请记住,CL是受不同Lisps启发的标准,它无助于强制执行一致的术语。
通常的兼并" f"这样的地方修改操作(例如,setf,incf,alexandria:deletef)有点零星,并且也适用于许多地方访问者(例如,aref,getf)。
所有setf扩展器都应以f
结尾,但并非以f
结尾的所有内容都是setf扩展器。例如,aref
的名称来自 array 和 reference ,并且不是宏。
...但似乎它可能导致一些使用地点修改的实际应用程序中的编程错误。
大多数数据都是可变的(见评论);一旦你在CL中编写代码时考虑到这一点,你就要注意不要修改你自己没有创建的数据。至于在无意中使用破坏性操作代替非破坏性操作,我不知道:我想它可能会发生,sort
或delete
,也许是你第一次使用它们。在我看来,delete
比remove
更强大,更具破坏性,但可能是因为我已经知道其中的差异。
由于不同的实现可以不同地处理场所结果,因此不能影响可移植性吗?
如果您想要便携性,请遵循规范,这不能提供太多保证w.r.t.应用了哪些破坏性操作。以DELETE
为例(强调我的):
序列可能被破坏并用于构建结果;但是,结果可能会或可能不会与序列相同。
假设列表的修改方式,或者即使它被修改,也是错误的。实际上,您可以在最小实现中将delete
实现为remove
的别名。在所有情况下,您都使用函数的返回值(delete
和remove
都具有相同的签名)。
我将破坏性的common-lisp序列操作划分为两个类别,对应于"参数返回"和"操作返回"。
根本不清楚这些类别应该代表什么。这些定义是你想到的吗?
参数返回操作是将其参数之一作为返回值返回,可能已修改。
操作返回操作是一个结果基于其参数之一的操作,并且可能与该参数相同,但不一定是。
操作返回的定义非常模糊,包含破坏性和非破坏性操作。我会将cons
归类为因为它不会返回其中一个论点; OTOH,这是一个纯粹的功能操作。
除了破坏性或非破坏性之外,我还没有真正了解这些类别提供的内容。
假设您编写了一个函数(remote host key)
,它从远程键/值数据存储区获取值。假设您定义(setf remote)
以便更新远程值。
您可能希望(setf (first (remote host key)) value)
:
但是,步骤3通常不会发生:本地列表已就地修改(这是最有效的替代方案,但它使setf扩展在更新方面有点懒惰)。您可以定义一组新的宏,例如始终使用DEFINE-SETF-EXPANDER
实现整个往返。
答案 1 :(得分:1)
让我试着通过介绍一些概念来解决你的问题。
我希望它可以帮助您巩固您的知识并找到有关此主题的剩余答案。
第一个概念是非破坏性与破坏性行为。
非破坏性的功能不会改变传递给它的数据。
具有破坏性的功能可能会改变传递给它的数据。
您可以将(非)破坏性应用于除单一功能之外的其他功能。例如,如果函数存储传递给它的数据,比如在对象的插槽中,那么破坏性的 ness 取决于该对象的行为,其他操作,事件等等。
立即修改其参数的函数的约定是(通常)前缀为n
。
约定并没有相反的方式,有很多以n
开头的函数(例如not
/ null
,nth
,{{ 1}},ninth
,notany
,notevery
等。)还有一些值得注意的例外情况,例如numberp
,delete
,merge
和sort
。自然掌握它们的唯一方法是时间/经验。例如,每当您看到一个您还不知道的功能时,请始终参考HyperSpec。
此外,您通常需要存储某些破坏性函数的结果,例如stable-sort
和delete
,因为它们可能会选择跳过列表的头部或根本不具有破坏性。 sort
实际上可能会返回空列表delete
,这是无法通过修改后的利弊获得的。
第二个概念是广义参考。
广义引用是任何可以保存数据的引用,例如变量,缺点的汽车和cdr,数组或散列表的元素位置,对象的插槽等。
对于每个容器数据结构,您需要知道特定的修改功能。但是,对于某些通用引用,可能没有修改它的函数,例如局部变量,在这种情况下仍然有特殊的形式来修改它。
因此,为了修改任何通用引用,您需要知道其修改形式。
与广义引用密切相关的另一个概念是地方。标识广义引用的表单称为地点。或者换句话说,一个地方是代表一般参考的书面方式(形式)。
对于每种地方,你都有一个读者形式和一个作家形式。
记录了其中一些表单,例如使用变量的符号来读取它,并nil
要写入的变量,或setq
/ car
来读取和cdr
/ rplaca
写一个缺点。其他只记录为访问者,例如rplacd
从数组中读取;它的作者形式实际上没有记录。
要获取这些表单,您需要get-setf-expansion
。实际上,您还会获得一组变量及其初始化形式(将用作aref
),这些形式将由读者表单和/或编写器表单使用,以及一组变量(将被绑定到新的价值观)将由作家表格使用。
如果您之前使用过Lisp,那么您可能已经使用过let*
。 setf
是一个宏,它生成在扩展范围(环境)内运行的代码。
本质上,它的行为就像使用setf
一样,为变量生成get-setf-expansion
形式并初始化表单,为writer变量生成额外的绑定以及value(s)形式的结果和在所有这些环境中调用编写器表单。
例如,让我们定义一个let*
宏,它只占用一个地方和一个新值形式:
my-setf1
然后,您可以将(defmacro my-setf1 (place newvalue &environment env)
(multiple-value-bind (vars vals store-vars writer-form reader-form)
(get-setf-expansion place env)
`(let* (,@(mapcar #'(lambda (var val)
`(,var ,val))
vars vals))
;; In case some vars are used only by reader-form
(declare (ignorable ,@vars))
(multiple-value-bind (,@store-vars)
,newvalue
,writer-form
;; Uncomment the next line to mitigate buggy writer-forms
;;(values ,@store-vars)
))))
定义为:
my-setf
这种宏有一个约定,后缀为(defmacro my-setf (&rest pairs)
`(progn
,@(loop
for (place newvalue) on pairs by #'cddr
collect `(my-setf1 ,place ,newvalue))))
,例如f
本身,setf
,psetf
,shiftf
,{ {1}},rotatef
,incf
和decf
。
同样,惯例并没有相反的方式,有些运算符以getf
结尾,例如remf
,f
和aref
,它们是函数,svref
是条件执行特殊运算符。而且,有一些值得注意的例外,例如find-if
,if
,push
,pushnew
,pop
,ldb
和{{1 }}
根据您的观点,更多的运营商具有隐含的破坏性,即使没有有效标记。
例如,每个定义运算符(例如宏mask-field
,assert
,check-type
,defun
,defpackage
,函数defclass
)更改全局环境或临时环境,例如编译环境。
其他人,例如defgeneric
,defmethod
和load
,取决于他们执行的表单。对于compile-file
,它还取决于它将编译环境与启动环境隔离的程度。
其他运营商,例如compile
,eval
,compile-file
,makunbound
,fmakunbound
,intern
,export
,{ {1}},shadow
,use-package
,rename-package
,adjust-array
,vector-push
,vector-push-extend
,vector-pop
,{{1 }},remhash
和clrhash
,(或多或少)显然有副作用。
而这最后一个概念可能是最广泛的。通常,副作用被视为一种环境中的任何可观察的变化。因此,不改变数据的功能通常被认为没有副作用。
然而,这是不明确的。您可能会认为所有代码执行都会产生副作用,具体取决于您定义的环境或您可以测量的内容(例如,消耗的配额,CPU时间和实时,已用内存,GC开销,资源争用,系统温度,能耗,电池消耗)。
注意:示例列表都不详尽。