在Scheme中,let和let*有什么区别让人感到困惑?

29

请问有人能简单解释一下它们的区别吗?我认为我没有从我查阅的教材/网站中理解这个概念。


2
可能是Scheme中let和let*的混淆的重复问题。 - David Pfeffer
4
似乎不是重复问题。那个问题询问了关于嵌套的letlet*的非常具体的交互,而这个问题则是要求一般概述。 - Inaimathi
机器执行的简单易懂的人类解释:混乱不堪。 - Nishant
2个回答

32

Let是并行的,(有点儿;请见下文) let*是顺序的。 Let翻译为

((lambda(a b c)  ... body ...)
  a-value
  b-value
  c-value)

但是let*却作为
((lambda(a)
    ((lambda(b)
       ((lambda(c) ... body ...)
        c-value))
     b-value))
  a-value)

因此,它创建了嵌套的作用域块,其中b-value表达式可以引用ac-value表达式可以同时引用baa-value 属于外部作用域。这也等价于

(let ((a a-value))
  (let ((b b-value))
    (let ((c c-value))
      ... body ... )))

还有letrec,允许递归绑定,其中所有变量和表达式都属于一个共享作用域,并且可以相互引用(但在初始化方面存在一些注意事项)。它等价于

(let ((a *undefined*) (b *undefined*) (c *undefined*))
  (set! a a-value)
  (set! b b-value)
  (set! c c-value)
  ... body ... )

(在Racket中, 也可在Scheme中使用letrec*,自R6RS以来), 或者

(let ((a *undefined*) (b *undefined*) (c *undefined*))
  (let ((_x_ a-value) (_y_ b-value) (_z_ c-value))   ; unique identifiers
    (set! a _x_)
    (set! b _y_)
    (set! c _z_)
    ... body ... ))

(在Scheme中)。

更新: let 实际上并不会并行地计算其值表达式,只是它们都在出现 let 表单的相同初始环境中被计算。这也可以从基于 lambda 的翻译中清楚地看出:首先,值表达式在相同的外部环境中分别被计算,并收集结果值,然后为每个 id 创建新位置,并将值放入其位置。如果其中一个值表达式改变了由后续值表达式访问的存储(例如数据,如列表或结构),则仍然可以看到顺序性。


如果它实际上不是并行的,那么使用let的价值是什么?我默认使用它,因为这似乎是其他人使用的约定,每次我遇到混淆的错误时,我都会将其更改为let*,这引出了一个问题,为什么不一开始就始终使用let*呢? - Joseph Garvin
@JosephGarvin let* 在精神上被认为是“命令式”的,而 let 则是声明式的。它在概念上是并行的,因为所有 RHS 都在同一个环境中(外部环境)进行评估。唯一能看到 let 初始化顺序的方法是如果初始化表达式改变结构,这在“声明式”范例下是不受欢迎的。所以如果我们不进行突变,对于我们来说,它是并行的,即我们不需要关注初始化的时间,就像我们需要关注 let* 一样。越少关注确切的时间,声明式范例就越好。 - Will Ness
如果它们改变了_shared_结构,那就会出现问题。如果该结构被封装起来,其变异不可从外部观察到,则可以对其进行变异(如果出于效率考虑需要这样做)。 - Will Ness

30
如果您使用let,则无法引用出现在同一let表达式中的其他绑定。
例如,下面的代码将无法运行:
(let ((x 10)
      (y (+ x 6))) ; error! unbound identifier: x
  y)

但是如果您使用 let*,就可以引用在同一 let* 表达式中出现的 前面 的绑定:

(let* ((x 10)
       (y (+ x 6))) ; works fine
  y)
=> 16

文档中已经包含了所有这里的内容。


1
我在文档中(您链接指向的当前版本为5.3.6)没有清楚地看到,所以我也感到困惑。关于 let 的文档说:“第一种形式从左到右评估 val-exprs ...”,因此不清楚它们是否并行评估。 - Alexey
1
@Alexey 它不会并行评估它们。正如文档所述,*"第一种形式从左到右评估 val-exprs,为每个 id 创建一个新位置,并将值放入位置中"* -- 这意味着,首先它们被评估并收集结果值,然后才为每个 id 创建新位置,并将值放入其位置中。如果其中一个 val-exprs 改变了后续访问的存储(即数据,如列表或结构),则仍然可以看到顺序性。 - Will Ness
尽管这解释了区别,但它并没有真正解释为什么首先存在这种区别。我曾经使用过的所有其他语言都将每个声明视为单独的顺序绑定,可以依赖于先前的绑定。实际上,在ALGOL派生语言中通常根本没有“同时”声明的概念,它们总是按某种顺序排列,因此就好像您始终在使用let*。我不明白始终使用let*有什么缺点,那么为什么这种令人困惑的怪异性仍然存在呢?保留区别是否允许表达任何新内容? - Joseph Garvin
使用 let,您可以按任何顺序甚至并行执行赋值操作(更符合函数式编程的方式),而 let* 强制执行顺序并创建对先前变量值的依赖关系(这是一种非常过程化的编程方式)。@JosephGarvin - Óscar López
@ÓscarLópez,但据我所知,在实践中,Scheme 实现实际上从未并行评估它们?此外,您需要推断分派到其他线程是否值得。没有任何防止绑定的表达式触及相同状态,因此您无法确保并行执行是安全的。我必须想象野外的大多数 Scheme 代码都会在 let 绑定被随机重新排序时出错。 - Joseph Garvin
@JosephGarvin 如果你坚持严格的函数式编程(无变异,无副作用),代码怎么会出错呢?同意,在实践中它不是并行的 - 但只要我们限制自己在语言的函数子集中,它就可以是。函数式编程的原则之一是程序员不必负担控制流的责任,“let”有助于实现这一点,而“let*”则不然。但我偏离了主题:) Scheme不是最好的语言来进行这种讨论,因为它不是严格的FP。 - Óscar López

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接