4

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?

Mulan
  • 129,518
  • 31
  • 228
  • 259
  • added FP tag as I know many of you will recognize the utility of this pattern – Mulan Sep 09 '17 at 23:21
  • FFerror message: *ReferenceError: can't access lexical declaration 'n' before initialization*. Seems like the assignment is done after the scope is made just like with `let` and `const`. eg. `const v = v` doesn't work because the new variable has already shadowed `v` from the outer scope. – Sylwester Sep 09 '17 at 23:45
  • 1
    Using babel, it gives same error. So I'm assuming it will expand in a similar way.. `n=n` would give `var n = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : n;` So the outer 'n' has basically be scoped away by the new `var n` – Keith Sep 09 '17 at 23:45
  • assignment *after* scope creation; i'm guessing the spec must show this ordering somewhere – if it does, what a tragedy. – Mulan Sep 09 '17 at 23:53
  • 1
    The documentation doesn't handle variable shadowing, but it shows that the default values are evaluated every call and that later expressions can use earlier values as in a `letrec*`. Imagine you did something like this: `const test = (fun = (v) => v == 0 ? 0 : 1 + fun(v-1), v = 10) => fun(v)`. Notice that the expression uses the bound name in its recursion, which wouldn't have worked unless `fun` existed at closure creation time. – Sylwester Sep 10 '17 at 00:02
  • @Sylwester it's all too obvious now why good languages like racket give us the freedom to choose the style/form of bindings – beyond just `let` or `const`, i mean – Mulan Sep 10 '17 at 00:14

1 Answers1

2

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.

Aadit M Shah
  • 72,912
  • 30
  • 168
  • 299
  • Aadit thanks for answer; i was worried i might have to expand the macro, but the other alternatives you provide are nice as well; cool to see a typed version of loop/recur ^_^ – Mulan Sep 11 '17 at 03:14
  • i recently had a different idea for syntax highlighting; im happy to share it with you once ive had a little more time to explore it – Mulan Sep 11 '17 at 03:15
  • Cool. Looking forward to it. =) – Aadit M Shah Sep 11 '17 at 03:52