OpenDSA 完整目录

Chapter 31 Variations on Parameter Passing

| 关于   «  1. 参数传递机制   ::   目录   ::   1. 程序设计语言中的类型  »

2. 惰性列表

2.1. 无限序列

实现按名调用

宏扩展可以通过双重文本替换(如 C++ 预处理器)或通过单次替换和动态作用域来实现。结果是调用者环境中对整个函数体进行求值。但是如何实现按名调用呢?如何在调用者环境中求值参数,而在被调用者环境中求值函数的其余部分?

我们不是简单地传递参数的文本表示,而是传递一个不带参数的匿名函数,该函数返回参数。这种匿名函数被称为 thunk。

理解在调用函数之前评估参数与传递 thunk 之间的区别,就是理解

\[\begin{eqnarray*} 7 \end{eqnarray*}\]

和

\[\begin{eqnarray*} \verb+function ( ) { return 7; }+ \end{eqnarray*}\]

前者,当作为参数传递时,已经求值。函数可以使用该值,而无需对其做任何其他操作。然而,后者,当作为参数传递时,需要调用 thunk 来“解包”函数在其计算中应使用的值。

与在调用函数之前评估参数并使用该值不同,每次在函数体中引用参数时,都会评估 thunk 以获取参数的值。评估过程通常被称为 解冻 thunk。

如果 thunk 包含对自由变量的引用,例如以下示例中的 x:

\[\begin{eqnarray*} \verb!function ( ) { return x + 7; }! \end{eqnarray*}\]

此时,被调用函数(即接收 thunk 作为参数的函数)将能够访问在调用者环境中定义的自由变量。这是因为 thunk 是一个函数,换句话说,它是一个闭包,包含了创建 thunk 时存在的环境(即包含 x 定义的调用者环境)。

按名调用的链表

为了说明 thunk 的用法,我们将实现按名称调用的列表,这种方式与 Haskell 中默认使用的列表方式类似,或者作为程序员在 Python 和 Scala 中选择的选项。按名称调用的列表本质上给你提供了 惰性列表,我们会看到它们也可以被视为“无限序列”。这种视角提供了一种非常不同的方法来处理此类列表。

下面是我们即将在无限序列 JavaScript 模块中提供的部分函数的文档,该模块称为 is 模块。

序列的构造函数(即 cons 函数)接受两个参数,即我们希望在序列头部放置的元素,以及一个延迟求值的 thunk,该 thunk 若需要访问第一个元素之后的部分则返回序列的尾部。 为了简化起见,我们只操作整数无限序列。

// Construct a new sequence comprised of the given integer and thunk
var cons = function (n,thunk) { ... };

// Get the first integer in the  sequence
var hd = function (seq) { ... };

// Get the infinite sequence following the first element.  This
// will itself be in the form of an integer followed by a thunk
var tl = function (seq) { ... };

// Return the (finite, non-lazy) list containing the first n
// integers in the given sequence
var take = function (seq,n) { ... };

以下幻灯片演示了我们如何利用这些操作来构建并暴露无限个 1 的序列的若干部分。

Settings

Proficient Saving... Error Saving
Server Error
Resubmit

现在让我们关注 is 模块中这四个基本函数,即 cons, hd, tl 和 take,是如何实现的。

// Construct a new sequence comprised of the given integer and thunk
var cons = function (x, thunk) {
  return [x, thunk];
};

// Get the first integer in the  sequence
var hd = function (seq) {
  return seq[0];
};

// Get the infinite sequence following the first element.  This
// will itself be in the form of an integer followed by a thunk
var tl = function (seq) {
  return thaw(seq[1]);
};

// thaw is a helper function for tl. It returns the result
// of evaluating the function given as argument
var thaw = function (thunk) { return thunk(); };

// Return the (finite, non-lazy) list containing the first n
// integers in the given sequence
var take = function (seq, n) {
  if (n === 0)
    return [];
  else {
    // Get a copy of the result of recursive call with n - 1
    var result = take(tl(seq), n - 1).slice(0); // slice(0) gives a copy of the array
    // And use Javascript's unshift to put the hd at the beginning of result
    result.unshift(hd(seq));
    return result;
  }
};

到目前为止,我们所能创建的序列仅是一个由全 1 组成的无聊序列。

  • from 操作:

Settings

Proficient Saving... Error Saving
Server Error
Resubmit

  • map 操作

Settings

Proficient Saving... Error Saving
Server Error
Resubmit

  • 过滤器 操作

Settings

Proficient Saving... Error Saving
Server Error
Resubmit

  • 删除 操作:

Settings

Proficient Saving... Error Saving
Server Error
Resubmit

  • 迭代操作:

Settings

Proficient Saving... Error Saving
Server Error
Resubmit

埃拉托斯特尼筛法:利用惰性列表的一个示例

计算各种素数的需求出现在多种应用中,例如公钥加密。

Settings

Proficient Saving... Error Saving
Server Error
Resubmit

然而,从该算法的实用性角度来看,存在一个问题。

Settings

Proficient Saving... Error Saving
Server Error
Resubmit

按需调用

我们的无限序列的按名调用实现与 Haskell 中的实现方式有何不同?在 Haskell 中, is.tl 和 is.take 函数的对应实现使用的是 按需调用 而非 按名调用。在按需调用中,当 thunk 第一次被解冻后,其返回值会被存储(即缓存)。这样做效率更高,因为 thunk 永远不会被解冻超过一次。

现在,机会来了,你可以在以下问题中练习无限序列。

以下问题将帮助你更好地理解创建按名调用无限序列的代码。

2.2. 练习无限序列

以下问题将帮助您编写递归代码来处理无限序列。要获得该问题的学分,您必须连续三次正确完成这个随机问题。

2.3. 练习与无限序列 (2)

以下问题回顾了序列的递归定义。要获得该问题的学分,你必须连续三次正确完成这个随机化问题。

2.4. 练习与无限序列(3)

以下问题涉及序列递归定义的另一个例子。

   «  1. 参数传递机制   ::   目录   ::   1. 程序设计语言中的类型  »

关闭窗口