以下是一个语法上有效的JavaScript程序 - 只是,它的行为与我们预期的方式不同。问题的标题应该有助于你的眼睛放大到问题区域
const recur = (...args) =>
({ type: recur, args })
const loop = f =>
{
let acc = f ()
while (acc.type === recur)
acc = f (...acc.args)
return acc
}
const repeat = n => f => x =>
loop ((n = n, f = f, x = x) => // The Problem Area
n === 0
? x
: recur (n - 1, f, f (x)))
console.time ('loop/recur')
console.log (repeat (1e6) (x => x + 1) (0))
console.timeEnd ('loop/recur')
// Error: Uncaught ReferenceError: n is not defined

如果我使用唯一标识符,程序将完美运行
const recur = (...args) =>
({ type: recur, args })
const loop = f =>
{
let acc = f ()
while (acc.type === recur)
acc = f (...acc.args)
return acc
}
const repeat = $n => $f => $x =>
loop ((n = $n, f = $f, x = $x) =>
n === 0
? x
: recur (n - 1, f, f (x)))
console.time ('loop/recur')
console.log (repeat (1e6) (x => x + 1) (0)) // 1000000
console.timeEnd ('loop/recur') // 24 ms

只有这没有意义。让我们来谈谈现在没有使用$
- 前缀的原始代码。
当评估loop
的lambda时,n
收到的repeat
在lambda的环境中可用。将内部n
设置为外n
的值应该有效shadow外部n
。但相反,JavaScript将此视为某种问题,内部n
会导致undefined
的分配。
这对我来说似乎是一个错误,但我在阅读规范时很糟糕,所以我不确定。
这是一个错误吗?
答案 0 :(得分:2)
我猜你已经弄明白为什么你的代码不起作用了。默认参数的行为类似于递归let绑定。因此,当您编写n = n
时,您将新声明的(但undefined
)变量n
分配给自己。就个人而言,我认为这很有道理。
因此,您在评论中提到了Racket,并评论了Racket如何允许程序员在let
和letrec
之间进行选择。我喜欢将这些绑定与Chomsky hierarchy进行比较。 let
绑定类似于常规语言。它不是非常强大但允许变量阴影。 letrec
绑定类似于递归可枚举语言。它可以做任何事情,但不允许变量阴影。
由于letrec
可以执行let
可以执行的所有操作,因此您根本不需要let
。一个主要的例子是Haskell,它只有递归let绑定(不幸的是叫let
而不是letrec
)。现在的问题是,像Haskell这样的语言是否也应该有let
绑定。要回答这个问题,让我们看看下面的例子:
-- Inserts value into slot1 or slot2
insert :: (Bool, Bool, Bool) -> (Bool, Bool, Bool)
insert (slot1, slot2, value) =
let (slot1', value') = (slot1 || value, slot1 && value)
(slot2', value'') = (slot2 || value', slot2 && value')
in (slot1', slot2', value'')
如果Haskell中的let
没有递归,那么我们可以将此代码编写为:
-- Inserts value into slot1 or slot2
insert :: (Bool, Bool, Bool) -> (Bool, Bool, Bool)
insert (slot1, slot2, value) =
let (slot1, value) = (slot1 || value, slot1 && value)
(slot2, value) = (slot2 || value, slot2 && value)
in (slot1, slot2, value)
那么为什么Haskell没有非递归的let绑定呢?嗯,使用不同的名称肯定有一些优点。作为编译器编写者,我注意到这种编程风格类似于single static assignment form,其中每个变量名只使用一次。通过仅使用变量名称一次,程序变得更容易让编译器进行分析。
我认为这也适用于人类。使用不同的名称有助于人们阅读您的代码来理解它。对于编写代码的人来说,可能更希望重用现有名称。但是,对于使用不同名称读取代码的人,可以防止由于所有内容看起来相同而导致的任何混淆。事实上,Douglas Crockford(经常被吹捧的JavaScript大师)advocates context coloring来解决类似的问题。
无论如何,回到手头的问题。我可以通过两种方式来解决您的问题。第一个解决方案是简单地使用不同的名称,这就是你所做的。第二种解决方案是模拟非递归let
表达式。请注意,在Racket中,let
只是一个扩展为左左lambda表达式的宏。例如,请考虑以下代码:
(let ([x 5])
(* x x))
此let
表达式将宏扩展为以下左左边的lambda表达式:
((lambda (x) (* x x)) 5)
事实上,我们可以使用反向应用程序运算符(&)
在Haskell中执行相同的操作:
import Data.Function ((&))
-- Inserts value into slot1 or slot2
insert :: (Bool, Bool, Bool) -> (Bool, Bool, Bool)
insert (slot1, slot2, value) =
(slot1 || value, slot1 && value) & \(slot1, value) ->
(slot2 || value, slot2 && value) & \(slot2, value) ->
(slot1, slot2, value)
本着同样的精神,我们可以手动解决您的问题"宏扩展" let
表达式:
const recur = (...args) => ({ type: recur, args });
const loop = (args, f) => {
let acc = f(...args);
while (acc.type === recur)
acc = f(...acc.args);
return acc;
};
const repeat = n => f => x =>
loop([n, f, x], (n, f, x) =>
n === 0 ? x : recur (n - 1, f, f(x)));
console.time('loop/recur');
console.log(repeat(1e6)(x => x + 1)(0)); // 1000000
console.timeEnd('loop/recur');

此处,我不是将默认参数用于初始循环状态,而是直接将它们传递给loop
。您可以将loop
视为Haskell中的(&)
运算符,它也会进行递归。实际上,这段代码可以直接音译成Haskell:
import Prelude hiding (repeat)
data Recur r a = Recur r | Return a
loop :: r -> (r -> Recur r a) -> a
loop r f = case f r of
Recur r -> loop r f
Return a -> a
repeat :: Int -> (a -> a) -> a -> a
repeat n f x = loop (n, f, x) (\(n, f, x) ->
if n == 0 then Return x else Recur (n - 1, f, f x))
main :: IO ()
main = print $ repeat 1000000 (+1) 0
正如您所看到的,您根本不需要let
。 let
可以完成的所有事情也可以由letrec
完成,如果你真的想要变量阴影,那么你可以手动执行宏扩展。在Haskell中,您甚至可以更进一步,使用The Mother of all Monads使代码更漂亮。