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.