Значение параметра по умолчанию не определено; это ошибка JavaScript?
Ниже приведена синтаксически верная 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
оценивается, n
как получено repeat
, доступен в среде лямбды. Установка внутреннего n
к внешнему n
ценность должна эффективно затенять внешнее n
, Но вместо этого JavaScript видит в этом какую-то проблему и внутреннюю n
приводит к назначению undefined
,
Мне это кажется ошибкой, но я не могу читать спецификацию, поэтому не уверен.
Это ошибка?
1 ответ
Я думаю, вы уже поняли, почему ваш код не работает. Аргументы по умолчанию ведут себя как рекурсивные привязки let. Следовательно, когда вы пишете n = n
Вы назначаете только что объявленный (но еще undefined
) переменная n
к себе. Лично я думаю, что это имеет смысл.
Итак, вы упомянули Racket в своих комментариях и отметили, как Racket позволяет программистам выбирать между let
а также letrec
, Мне нравится сравнивать эти привязки с иерархией Хомского. 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'')
Если let
в Haskell не было рекурсивным, тогда мы могли бы написать этот код как:
-- 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? Ну, определенно есть смысл использовать разные имена. Как автор компиляции, я замечаю, что этот стиль программирования похож на единственную статическую форму присваивания, в которой каждое имя переменной используется ровно один раз. Используя имя переменной только один раз, программа становится проще для анализа компилятором.
Я думаю, что это относится и к людям. Использование разных имен помогает людям, читающим ваш код, понять его. Для человека, пишущего код, было бы более желательно повторно использовать существующие имена. Тем не менее, для человека, читающего код с использованием разных имен, можно избежать путаницы, которая может возникнуть из-за того, что все выглядит одинаково. Фактически, Дуглас Крокфорд (часто рекламируемый JavaScript-гуру) выступает за раскрашивание контекста, чтобы решить аналогичную проблему.
Во всяком случае, вернемся к данному вопросу. Есть два возможных способа решить вашу непосредственную проблему. Первое решение - просто использовать разные имена, что вы и сделали. Второе решение заключается в эмуляции нерекурсивных let
выражения. Обратите внимание, что в Racket, let
это просто макрос, который расширяется до левого-левого-лямбда-выражения. Например, рассмотрим следующий код:
(let ([x 5])
(* x x))
это let
выражение будет расширено макросом до следующего выражения left-left-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
Как вы видите, вам не нужно let
совсем. Все, что может быть сделано let
также может быть сделано letrec
и если вы действительно хотите затенение переменных, то вы можете просто выполнить расширение макроса вручную. В Haskell вы могли бы пойти еще дальше и сделать свой код красивее, используя Мать всех монад.