Default parameter value undefined; is this a JavaScript Bug?

房东的猫 提交于 2021-01-27 06:46:25

问题


Below is a syntactically valid javascript program – only, it doesn't behave quite the way we're expecting. The title of the question should help your eyes zoom to The Problem Area

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

If instead I use unique identifiers, the program works perfectly

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

Only this doesn't make sense. Let's talk about the original code that doesn't use the $-prefixes now.

When the lambda for loop is being evaluated, n as received by repeat, is available in the lambda's environment. Setting the inner n to the outer n's value should effectively shadow the outer n. But instead, JavaScript sees this as some kind of problem and the inner n results in an assignment of undefined.

This seems like a bug to me but I suck at reading the spec so I'm unsure.

Is this a bug?


回答1:


I guess you already figured out why your code doesn't work. Default arguments behave like recursive let bindings. Hence, when you write n = n you're assigning the newly declared (but yet undefined) variable n to itself. Personally, I think this makes perfect sense.

So, you mentioned Racket in your comments and remarked on how Racket allows programmers to choose between let and letrec. I like to compare these bindings to the Chomsky hierarchy. The let binding is akin to regular languages. It isn't very powerful but allows variable shadowing. The letrec binding is akin to recursively enumerable languages. It can do everything but doesn't allow variable shadowing.

Since letrec can do everything that let can do, you don't really need let at all. A prime example of this is Haskell which only has recursive let bindings (unfortunately called let instead of letrec). The question now arises as to whether languages like Haskell should also have let bindings. To answer this question, let's look at the following example:

-- 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'')

If let in Haskell wasn't recursive then we could write this code as:

-- 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)

So why doesn't Haskell have non-recursive let bindings? Well, there's definitely some merit to using distinct names. As a compiler writer, I notice that this style of programming is similar to the single static assignment form in which every variable name is used exactly once. By using a variable name only once, the program becomes easier for a compiler to analyze.

I think this applies to humans as well. Using distinct names helps people reading your code to understand it. For a person writing the code it might be more desirable to reuse existing names. However, for a person reading the code using distinct names prevents any confusion that might arise due to everything looking the same. In fact, Douglas Crockford (oft-touted JavaScript guru) advocates context coloring to solve a similar problem.


Anyway, back to the question at hand. There are two possible ways that I can think of to solve your immediate problem. The first solution is to simply use different names, which is what you did. The second solution is to emulate non-recursive let expressions. Note that in Racket, let is just a macro which expands to a left-left-lambda expression. For example, consider the following code:

(let ([x 5])
  (* x x))

This let expression would be macro expanded to the following left-left-lambda expression:

((lambda (x) (* x x)) 5)

In fact, we can do the same thing in Haskell using the reverse application operator (&):

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)

In the same spirit, we can solve your problem by manually "macro expanding" the let expression:

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');

Here, instead of using default parameters for the initial loop state I'm passing them directly to loop instead. You can think of loop as the (&) operator in Haskell which also does recursion. In fact, this code can be directly transliterated into 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

As you can see you don't really need let at all. Everything that can be done by let can also be done by letrec and if you really want variable shadowing then you can just manually perform the macro expansion. In Haskell, you could even go one step further and make your code prettier using The Mother of all Monads.



来源:https://stackoverflow.com/questions/46135908/default-parameter-value-undefined-is-this-a-javascript-bug

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!