Scheme 和 Lisp 最佳实践:Scheme 是递归,Lisp 不是递归?

Scheme and Lisp best practices: recursion yes for Scheme, no for Lisp?

据我所知——如果我错了我会被纠正——好的Scheme实践是做任何需要循环、递归重复的事情,此外,溢出不会成为问题,因为尾-递归是内置的。然而,Lisp 没有溢出保护,因此,所有循环迭代宏(loopwhile 等)。因此,在现实世界的 Lisp 中,您通常不使用递归,而 Scheme 却只需要递归。

如果我的假设是正确的,有没有一种方法可以 "pure" 使用 Lisp,而不会有溢出的风险?还是在 Lisp 中使用递归太过逆流?我记得 The Little Schemer 他们是如何用递归给你一个彻底的锻炼。还有一个更早的版本叫做 The Little Lisper。它是否为您提供了 Lisp 中相同的递归练习?然后 Land of Lisp 让我对循环或递归是 "best practice."

感到困惑

我想做的是决定是在 Emacs Org 模式中使用 Racket,还是只为初学者使用内置的 Elisp。我希望学生尽可能保持纯粹的功能,例如,我不想解释递归这个非常新的和困难的主题,然后说 "Oh, but we won't be using it..."

As I understand -- and I'm here to be corrected if wrong -- good Scheme practice is to do anything requiring looping, repeating with recursion, and, furthermore, overflow won't be a problem because tail-recursion is built-in.

据我所知这是正确的。

Lisp, however, doesn't have protection from overflow [...]

不完全是。大多数 self-respective Common Lisp 实现提供 tail-call 合并(有一些限制,请参阅 https://0branch.com/notes/tco-cl.html). The difference is that there is no requirements from the language specification to have it. That grants compiler writers more freedom when implementing various Common Lisp features. Emacs Lisp does not have TCO, except in the form of libraries like recur or tco (self-recursion)。

... hence, all the loop iterative macros (loop, while, etc). And so in real-world Lisp you usually don't use recursion, while Scheme wants it all but exclusively.

差异主要在于文化。取一个 REPL (Read-Eval-Print-Loop)。我认为将交互视为循环而不是无限 tail-recursive 函数更有意义。不知何故,纯函数式语言似乎不愿意将循环视为原始控制结构,而迭代过程被认为更优雅。为什么不能两者兼得?

If my assumptions are true, is there a way to be "pure" with Lisp and not risk overflow? Or is this too much swimming against the stream to use recursion in Lisp?

你当然可以在 Lisp 中使用递归,只要你不滥用它,小程序不应该这样。例如,OCaml 中的 map 不是 tail-recursive,但人们仍然经常使用它。如果您使用 SBCL,手册中有一节解释了如何执行 tail-call 消除。

What I'm trying to do is decide whether to use Racket inside of Emacs Org-mode or just use built-in Elisp for beginner students. I want students to stay as purely functional as possible, e.g., I don't want to explain the very new and difficult topic of recursion, then say "Oh, but we won't be using it..."

如果你想教授函数式编程,请使用更函数式的语言。换句话说,在Racket和Emacs Lisp之间,我认为Racket更适合学生。用Racket教函数式编程的资料比较多,还有Typed Racket

在球拍中the looping construct of choice is for.

Racket(和Scheme一样)也有TCO,所以原则上可以 使用函数调用编写所有循环结构——事实并非如此 很方便。

典型的 Lisp 方言和各种 Scheme 方言之间存在很多差异:

方案

  • 需要尾调用优化
  • 支持尾递归优于命令式循环结构
  • 不喜欢命令式控制流
  • 提供功能抽象
  • 尝试将控制结构映射到函数(参见示例 CALL-WITH-CURRENT-CONTINUATION)
  • 实现一些循环宏作为扩展

Lisp

部分内容适用于 Lisp 方言,例如 Emacs Lisp、Common Lisp 或 ISLisp。

  • 具有低级控制流,例如类似 GOTO 的构造:不要在用户级代码中使用它们,有时它们可​​能在宏中很有用。
  • 不标准化甚至不提供尾调用优化:为了获得最大的可移植性,代码中不需要 TCO
  • 提供简单的宏:DO、DOTIMES、DOLIST:在直接适用的地方使用它们
  • 提供复杂的宏:LOOP 或 ITERATE 作为库
  • 提供功能抽象,如 MAP、MAPCAR、REDUCE

实现通常对堆栈大小有硬性限制,这使得 non-TCO 递归函数存在问题。通常可以预先设置较大的堆栈大小或在运行时扩展堆栈大小。

尾调用优化也与特殊变量或类似变量等动态作用域结构不兼容。

对于 Common Lisp 中的 TCO 支持,请参阅:Tail Call Optimisation in Common Lisp Implementations

风格

  • 有些人更喜欢递归函数,但我一般不会使用它。一般来说,我(这是我个人的观点)更喜欢显式循环构造而不是一般的递归调用。其他人更喜欢递归函数的更数学方法的想法。
  • 如果有像 MAP 这样的 higher-order 功能,请使用它。
  • 如果循环代码变得更加复杂,使用功能强大的 LOOP 语句会很有用

Emacs Lisp 中没有尾调用优化 (TCO),所以尽管我个人非常喜欢递归,但我通常建议不要在 Elisp 中使用它(当然,除非它是唯一自然的情况)选择)。

Elisp 作为编程环境还算不错,但我必须同意"coredump" 并且会推荐 Racket,它对初学者有很好的支持。