请问有人能简单解释一下它们的区别吗?我认为我没有从我查阅的教材/网站中理解这个概念。
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
表达式可以引用a
,c-value
表达式可以同时引用b
和a
。 a-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 Garvinlet*
在精神上被认为是“命令式”的,而 let
则是声明式的。它在概念上是并行的,因为所有 RHS 都在同一个环境中(外部环境)进行评估。唯一能看到 let
初始化顺序的方法是如果初始化表达式改变结构,这在“声明式”范例下是不受欢迎的。所以如果我们不进行突变,对于我们来说,它是并行的,即我们不需要关注初始化的时间,就像我们需要关注 let*
一样。越少关注确切的时间,声明式范例就越好。 - Will Nesslet
,则无法引用出现在同一let
表达式中的其他绑定。(let ((x 10)
(y (+ x 6))) ; error! unbound identifier: x
y)
但是如果您使用 let*
,就可以引用在同一 let*
表达式中出现的 前面 的绑定:
(let* ((x 10)
(y (+ x 6))) ; works fine
y)
=> 16
文档中已经包含了所有这里的内容。
let
的文档说:“第一种形式从左到右评估 val-exprs
...”,因此不清楚它们是否并行评估。 - Alexeyval-exprs
,为每个 id
创建一个新位置,并将值放入位置中"* -- 这意味着,首先它们被评估并收集结果值,然后才为每个 id
创建新位置,并将值放入其位置中。如果其中一个 val-exprs
改变了后续访问的存储(即数据,如列表或结构),则仍然可以看到顺序性。 - Will Nesslet*
。我不明白始终使用let*
有什么缺点,那么为什么这种令人困惑的怪异性仍然存在呢?保留区别是否允许表达任何新内容? - Joseph Garvinlet
,您可以按任何顺序甚至并行执行赋值操作(更符合函数式编程的方式),而 let*
强制执行顺序并创建对先前变量值的依赖关系(这是一种非常过程化的编程方式)。@JosephGarvin - Óscar López
let
和let*
的非常具体的交互,而这个问题则是要求一般概述。 - Inaimathi