默认参数值未定义;这是一个JavaScript Bug吗?

时间:2017-09-09 23:10:40

标签: javascript syntax functional-programming specifications

以下是一个语法上有效的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的分配。

这对我来说似乎是一个错误,但我在阅读规范时很糟糕,所以我不确定。

这是一个错误吗?

1 个答案:

答案 0 :(得分:2)

我猜你已经弄明白为什么你的代码不起作用了。默认参数的行为类似于递归let绑定。因此,当您编写n = n时,您将新声明的(但undefined)变量n分配给自己。就个人而言,我认为这很有道理。

因此,您在评论中提到了Racket,并评论了Racket如何允许程序员在letletrec之间进行选择。我喜欢将这些绑定与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

正如您所看到的,您根本不需要letlet可以完成的所有事情也可以由letrec完成,如果你真的想要变量阴影,那么你可以手动执行宏扩展。在Haskell中,您甚至可以更进一步,使用The Mother of all Monads使代码更漂亮。