我正在阅读Paul Graham的OnLisp,试图更好地理解编程的函数式风格。在第3章中,他提到函数式程序"通过返回值来工作",而不是执行副作用。我不太明白这个陈述对如何操纵和表示过程的中间结果的影响。如果函数程序返回一个值,那么使用(let)
捕获它是否等同于 使用(lambda)
函数?
以下示例显示了函数的定义,(group)
,使用 lambda 函数捕获中间结果。(group)
采用列表、src
和数字n
,并将列表元素细分为大小为n
k
组。
(defun group (src n acc)
(funcall
#'(lambda (back)
(cond
((consp back)
(group back n (cons (subseq src 0 n) acc)))
(t
(reverse (cons src acc)))))
(nthcdr n src)))
函数的最后一行通过获取(nthcdr n src)
来获取列表的后分区。然后,此结果作为参数传递给 lambda 函数,该函数决定如何处理src
以参数为条件。这是纯粹的功能代码,没有副作用。另一方面,我本可以通过以下方式定义(group)
:
(defun group (src n acc)
(let ((back (nthcdr n src)))
(cond ((consp back)
(group back n (cons (subseq src 0 n) acc)))
(t
(reverse (cons src acc))))))
我们使用(let)
首先将变量back
绑定到(nthcdr n src)
。我不确定这个版本的(group)
的功能如何,因为(let)
以命令式的方式将变量绑定到值。
嗯,看待let
的一种方法是,它是lambda
特定用法的语法上更方便的形式。
为了澄清一些符号,在Common Lisp中,几种形式是等价的。
(function (lambda (...) ...))
可以编写为#'(lambda (...) ...)
,因为#'
是读取器宏。- 然后
#'(lambda (...) ...)
可以写成(lambda (...) ...)
,因为lambda
是一个展开(function (lambda (...) ...))
的宏。 - 最后
(funcall (lambda (...) ...) ...)
,相当于上面的(funcall (function (lambda (...) ...) ...)
,可以写成((lambda (...) ...) ...)
,作为复合形式的特例(见3.1.2.1.2.4)。
这些在 Lisp-1 中都不是必需的,但在 CL 中却是必需的。
下面我将写((lambda (...) ...) ...)
,而不是我认为格雷厄姆可能使用的笨拙(funcall #'(lambda (...) ...) ...)
。
所以,现在,重要的一点是:
(let ((x ...) ...) ...)
完全等同于((lambda (x ...) ...) ...)
。
虽然它在 CL 中不是以这种方式实现的(let
是一个特殊的运算符),但您可以将let
视为根据这样的lambda
定义的宏。 特别是这里有一个名为allow
的宏的定义,它类似于let
(当然,你不能在 CL 中重新定义let
本身,因此得名):
(defmacro allow (bindings &body decls/forms)
`((lambda ,(mapcan (lambda (b)
(typecase b
(null '()) ;elide ()'s in binding list as special caee
(symbol (list b))
(cons
(unless (eql 2 (list-length b))
(error "mutant binding ~S" b))
(list (first b)))
(t
(error "what even is ~S" b))))
bindings)
,@decls/forms)
,@(mapcan (lambda (b)
(typecase b
(null '())
(symbol (list 'nil))
(cons (list (second b)))
(t (error "what even is ~S" b))))
bindings)))
现在,例如,使用宏膨胀示踪剂:
> (allow ((x 1) (y 2)) (+ x y))
(allow ((x 1) (y 2)) (+ x y))
-> ((lambda (x y) (+ x y)) 1 2)
3
正如我所说,在CL中,let
不是这样定义的,因为它是一个特殊的运算符,但它可能是,并且可能有相应的let*
定义等等。 以下是allow*
的定义,let*
:
(defmacro allow* (bindings &body decls/forms)
(if (null (rest bindings))
`(allow ,bindings ,@decls/forms)
`(allow (,(first bindings))
(allow* ,(rest bindings) ,@decls/forms))))
(在这一点上,我可以嘲笑那些说"带有递归扩展的宏baaad,baaaad"的人。
所以从语言的语义来看,完全没有区别:(let (...) ...)
完全等同于((lambda (...) ...) ...)
。 特别是,由于任何涉及let
的程序都可以仅使用lambda
简单地重写为一个程序,并且lambda
是一个纯函数构造,那么let
也是一个纯函数构造。
那么在实际操作上有两个区别。
可读性。这是重要的一个。 程序不仅仅是指示机器做某事的方式:它们是将你的意图传达给其他人的一种方式。 对于几乎每个人(我认为,可能实际上对每个人来说)更容易阅读用英语写的代码
让x成为...,现在...涉及X的事情...
而不是用自然语言写的东西,但可能是
x将在此处有一个值,现在...涉及x...,值为 ...
在比较时,情况更糟
让X成为 ...而你就是...,现在...
与真正可怕的
x 和 y将在此处具有值,现在 ...,涉及x和y的东西 ...,并且值是......和。。。
这只是一种糟糕的编写方式:值与被绑定的变量相距甚远,当你最终到达它们时,没有指示哪个值属于哪个变量,最后有一个串行依赖关系,人类通常更难解析。
如果你遇到这样写的自然语言文本,你会纠正它,因为它很糟糕。 嗯,这就是let
的作用:它将难以阅读的lambda
形式转变为更容易理解的形式,其中初始值位于变量旁边。
可能的历史易于实施。我认为,如果let
被视为一种特殊情况,而不是简单地将其扩展为lambda
形式的宏,那么编译器可能曾经更容易。 我不确定这一点,特别是因为我相当确定我已经在我刚才找不到的地方读到let
起源于宏,但这似乎是合理的,let
是CL中的特殊运算符。 当然,我发现很难想象编译器不可能看到((lambda (...) ...) ...)
并以某种最佳方式编译该形式(即根本不编译函数),即使在很久以前编译器是由泥巴和鹅脂制成的。
我认为今天忽略第二个原因是安全的。
回答您的问题的要点:
第一
在计算机科学中,操作、函数或表达式被称为 如果它修改外部的某些状态变量值,则会产生副作用它的本地环境-- 维基百科
因此,使用局部变量来处理输入并不意味着您不进行函数式编程。重要的是你开发的函数的确定性:对于任何一组输入,函数应始终返回与输入相对应的相同输出。
当你想删除任何对本地状态的使用时,你正在尝试做纯粹的函数式编程:
纯函数式编程,函数式编程的一个子集 将所有函数视为确定性数学函数,或 纯函数。当一个纯函数被调用时,一些给定的 参数,它将始终返回相同的结果,并且不能 受任何可变状态或其他副作用的影响。-- 维基百科
这里重要的是知道你为什么要做函数式编程:
纯函数式编程的支持者声称,通过限制 副作用,程序可以有更少的错误,更容易调试和 测试,并且更适合形式验证。——维基百科。
我还会添加重要的东西,例如惰性计算和图形缩减(这应该会导致自动优化):
惰性求值不计算函数参数,除非 需要值来计算函数调用本身。
函数式惰性求值的常用实现策略 语言是图约简。默认使用延迟求值 在几种纯函数式语言中,包括 Miranda、Clean 和 哈斯克尔。-- 维基百科
Common Lisp可以进行惰性评估,但使用像CLAZY这样的框架。
使用函数式编程,并发很容易,因为此范式避免了可变状态。
因此,回到您的情况,group
实现都使用局部变量(当您有趣地调用它时,lambda 的参数是一个局部变量)。两者都使用尾递归,这是一件好事,并消除了对调用堆栈容量的依赖。
问题在于,两个实现中的局部变量back
都隐式依赖于代码所遇到的系统的限制,因为back
可能非常大。
如果您事先知道输入的最大大小,并且它可以完全存储在内存中,那么使用这些实现没有问题。但是,如果您的输入是几 EB 的数据,则最好使用流和生成器,并将本地状态简化为整数(您正在构建的当前组中的元素数),而不是存储整个序列和子序列。这样,由于函数式编程,您可以分配负载并具有并发性。
我想说,这是很多个人品味。 由于let
本身大多是由lambda
实现的。lambda
参数是局部变量,就像let
是一个变量一样。
(let ((a 1))
a)
;; is equivalent to:
((lambda (a)
a)
1)
我认为如果您使用let*
,它会变得相当有趣,因为现在您可以按顺序使用存储和重命名的中间值。 相比之下,嵌套 lambda 将非常庞大且难以理解。因为如您所见 - lambda 的输入出现在 lambda 调用的最后......
我喜欢使用let
,尤其是在改变一个或多个变量时 在过程或循环上使用setf
。
(let ((result '()))
(loop for i in '(1 3 7 11)
do (setf result (cons i result)))
result)
;;=> (11 7 3 1)
当然,这种情况是一个非常愚蠢的例子(可以在不使用let
的情况下表达得更简洁),但用于setf
的过程可能要复杂得多。
这个愚蠢的例子将表示为lambda
- 递归的尾部调用(我将使用defun
来命名lambda
):
(defun myfun (lst &optional (acc '()))
(cond ((null lst) acc)
(t (myfun (cdr lst) (cons (car lst) acc)))))
(myfun '(1 3 7 11))
;;=> (11 7 3 1)
我认为let
不那么冗长,而且很有用,特别是当一个人有几个这样的变量会被突变时。
可能当保存结果的变量应该突出显示时,let
表达式更好, 而如果应该更加强调该过程 - 并且是可重用的 - 那么应该使用lambda
或defun
- 这给了该过程一个名称。
但是,当然,您可以将lambda
/defun
包裹在let
表达式周围,为该过程命名...