问题
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