使用 let-values 而不是 let 有什么好处?

What is the benefit to use let-values instead of let?

我脑海中的例子是这样的:哪个更好?

示例 1:

(define (foo x)
  ...
  (values a b c))

(let-values (((a b c) (foo  42)))
    ...)

示例 2:

(define (foo x)
  ...
  (list a b c))

(let ((f (foo 42)))
  (let ((x (first f)) (y (second f)) (z (third f)))
      ...))

我的粗略猜测是第一种方法是最好的,因为在第二种方法中,每当调用 first/second/third 时,它都必须遍历列表。所以我的问题变成了:values 是如何工作的?它只是列表的语法糖还是使用其他东西? (例如数组)

如果这取决于实现,我会让你知道我正在使用鸡方案。

多个return值居然和列表不一样,有点奇怪。 valueslet-valuesdefine-values等用于处理多个return值。

如果您使用模式匹配宏,例如来自 match egg,那么当 returning 列表时,您将能够做完全相同的事情。

关于 multi-values 的想法来源的一些背景知识

多值(通常缩写为 "MV")是 Scheme 的一个略有争议的设计选择。它们使用起来有点笨拙,这导致一些人称它们为 "ugly"。但是它们自然地遵循具体化延续提供的 procedure-like 接口。让我稍微解压一下:

每当过程 "returns" 时,真正发生的是过程中最终表达式的值被传递给它的延续。这样,调用过程的表达式计算出一个值。

例如:在 (foo (+ 1 (* 2 6))) 中,(* 2 6) 的延续会将其值(即 * 过程中的最后一个表达式,无论它是什么)作为参数传递至 +。因此,在(foo (+ 1 <>))中,<>表示(* 2 6)的延续。那么,本例中(+ 1 (* 2 6))的延续就是用13调用foo(+ 1 (* 2 6))的结果。

如果您使用 call-with-current-continuation 来捕获这样的延续,您将获得一个您可以稍后调用的过程。您应该能够使用多个参数调用此过程是很自然的;唯一的问题是一个表达式如何实际产生多个值。

更具体地说,

(define (get-value) 1)

完全相同:

(define (get-value)
  (call-with-current-continuation
    (lambda (return-the-value)
      (return-the-value 1))))

所以,这也有道理:

(define (get-3-values)
  (call-with-current-continuation
    (lambda (cont)
      (cont 1 2 3))))

但是,调用 get-3-values 应该如何工作?每个表达式总是产生一个值(换句话说,每个常规过程调用的延续只接受一个值)。这就是为什么我们需要一个特殊的构造来创建一个实际接受多个值的延续:let-values 来自 SRFI-71 (or receive from SRFI-8)。

如果你只想使用标准结构,你可以使用call-with-values,但有点尴尬:

(call-with-values (get-3-values) (lambda (a b c) ...))

tl;博士

如果您可以调用具有多个值(参数)的过程,为什么它不能 return 多个值?这是有道理的,尤其是考虑到 Scheme 的核心处理延续,这模糊了 "argument" 和 "return value" 之间的界限。

设计选择:何时使用 MV,何时不使用?

现在,关于何时使用多个值而不是一个明确的列表是一个品味问题。如果该过程在概念上有多个 return 值,那么 return 多个值最有意义。例如,如果您获取 PDF 中页面的尺寸,图书馆可能有一个 get-dimensions 过程,其中 return 有两个值:宽度和高度。将它们返回到列表中会有些奇怪。当然,在这种特殊情况下,将设计更改为具有 get-heightget-width 可能更有意义,但如果操作的计算成本很高并且会产生两个值,则具有 get-dimension 而不是单独的(因为这将需要双倍的计算时间或一些复杂的缓存/记忆)。

特别是在 CHICKEN 中,还有一个使用多个值 returns 的设计原因:它隐式丢弃所有值,但如果多个值被传递给仅接受一个值的延续(在标准 Scheme "it is an error" 如果发生这种情况,那么实现可以自由地做任何他们想做的事)

举一个实际的例子,http-client egg(免责声明:我是它的作者)有像with-input-from-request这样的程序,其中return有几个值,但第一个值通常是最多的有用/有趣:这是从 URI 读取的结果。 returned 的其他值是请求的 URI,如果有任何重定向,它可能会有所不同,而响应 object,如果您需要提取 headers,这有时很有用从回应。所有这一切的结果是,您可以 (with-input-from-request "https://whosebug.com" #f read-string) 接收包含 Stack Overflow 主页的字符串,甚至不必费心处理构造请求 object 或挑选响应 object,但是如果您需要做更高级的事情,您可以选择这样做。

请注意,在某些方案中多个值的实现效率可能很低,因此出于实际原因,return object 可能更有效,例如列表、向量或记录(在 CHICKEN、MV速度很快,所以这不是问题)。

从语义的角度来看,没有一个比另一个更好。您可以使用列表、向量和记录,它们将提供相同的功能。 IMO 散列会稍微有利,因为那时您有访问器,因此以后可以增加接口。 JavaScript 只能 return 一个值,因此它使用对象,并且有了新的重组运算符,它变得非常有用。

除了词法闭包外,Scheme 并没有发明太多东西是从早期的 lisp 版本中派生出来的。就像 loop 宏能够 return 多个值一样意味着您不需要在堆上分配稍后需要 GC 处理的短期对象。当时机器的内存很少,GC 有时是用磁盘完成的。在机器级别上,return 通常是留在堆栈或寄存器中的东西。多个值将是堆栈上的两个值或使用两个寄存器。没有为封装分配堆意味着没有分配短期内存。

在兄弟语言 Common Lisp 中,多个值更有用,因为每个函数都可以在一个值的地方使用,然后只使用第一个值 returned。你会看到 Scheme 中的标准库缺少这个,有几个函数做同样的事情。 quotient/remainder 即 return 两个值,quotientremainder 即 return 这两个值中的每一个。