#javascript #sicp
#javascript #sicp
Вопрос:
Я читаю SICP в JS о не завершающемся примере троичных условий:
function is_even(n) {
return n % 2 === 0;
}
function expmod(base, exp, m) {
const half_exp = expmod(base, exp / 2, m);
return exp === 0 ? 1
: is_even(exp) ? half_exp * half_exp % m
: base * expmod(base, exp - 1, m) % m;
}
console.log(expmod(4, 3, 5))
Это объясняет, что:
Это сделало бы функцию не просто неэффективной, но и фактически не завершающейся! Проблема в том, что объявление константы появляется вне условного выражения, что означает, что оно выполняется даже при выполнении базового варианта exp === 0 .
Я просто не могу понять, когда exp === 0, он завершается на 1, но почему выполняется half_exp?
Комментарии:
1. Я не понимаю, как этот пример бесконечной рекурсии имеет какое-либо отношение к условному оператору… В принципе
function expmod() {expmod();}
… Не могли бы вы пояснить, почему, по вашему мнению, условный оператор имеет здесь какое-то значение?
Ответ №1:
Вы выполняете рекурсивный вызов в первой строке, которая выполняется независимо ни от чего. Это означает, что функция не завершается, то есть является бесконечным циклом.
function expmod(base, exp, m) {
const half_exp = expmod(base, exp / 2, m); // <- recursive call
// code ...
}
Рекурсивные вызовы всегда должны выполняться в паре с какой-либо проверкой, чтобы убедиться, что есть точка выхода.
Комментарии:
1. Я предположил, что
const half_exp = expmod(base, exp / 2, m);
это не рекурсивный вызов, потому что нет продолженияreturn
. кроме того,expmod(base, exp / 2, m)
может не оцениваться до тех пор, пока не будет применен half_exp.2. В JavaScript функция вычисляется, как только вы ее вызываете. Присвоение
expmod(base, exp / 2, m)
half_exp
будет означать, что рекурсивный вызов выполняется прямо здесь и тогда, а не при использованииhalf_exp
.
Ответ №2:
Часть, которую вы неправильно поняли, заключается в том, как и когда инициализируются переменные, а не в том, как работает троичный. Троичный будет работать так, как вы думали, если интерпретатор достиг его.
Вы поместили half_exp
переменную в условное выражение и ожидали, что она не будет оценивать свой инициализатор, пока он не будет использован.
Однако это не так, как это работает.
Все операторы инициализации переменных (оба var
, let
и const
) немедленно вычисляют свой инициализатор, когда элемент управления достигает оператора, не проверяя, используется ли переменная позже; и сохраняют значение инициализатора переменной.
Вы можете увидеть это, запустив следующий фрагмент:
const foo = console.log("I'm executed!")
//`foo` is never used, but the code will print "I'm executed!" anyway
Вы также можете подтвердить это, просмотрев спецификацию ECMAScript.
Лексическая привязка: инициализатор идентификатора привязки
- Пусть bindingId будет строковым значением BindingIdentifier .
- Пусть lhs будет ResolveBinding(bindingId).
- Если IsAnonymousFunctionDefinition(инициализатор) имеет значение true, то
a. Пусть значением будет NamedEvaluation инициализатора с аргументом bindingId .
- Ещё,
a. Пусть rhs будет результатом вычисления инициализатора *.
b. Пусть значение будет ? Получить значение (rhs).- Возвращает InitializeReferencedBinding(lhs, значение).
*: Выделение мое.
Итак, как вы можете видеть, интерпретатор не будет ждать использования переменной.
Это означает, что в вашем коде:
// v-------------------------------------------
function expmod(base, exp, m) { // |
const half_exp = expmod(base, exp / 2, m); // ---
// ^^^^^^--- This will always be called
// This line is not even reached!
return exp === 0 ? 1
: is_even(exp) ? half_exp * half_exp % m
: base * expmod(base, exp - 1, m) % m;
}
… у вас бесконечная рекурсия.
Чтобы обойти проблему, мы должны переместить этот вызов в условную часть. В вашем коде это просто, поскольку вместо того, чтобы записывать умножение на себя, мы можем повысить значение до его второй степени, исключив одну из ссылок:
function is_even(n) {
return n % 2 === 0;
}
function expmod(base, exp, m) {
return exp === 0 ? 1
: is_even(exp) ? expmod(base, exp / 2, m) ** 2 % m
: base * expmod(base, exp - 1, m) % m;
}
console.log(expmod(4, 3, 5)) //4
В других случаях, когда нет такого простого способа, мы могли бы переработать код иначе, например, используя if
s:
function is_even(n) {
return n % 2 === 0;
}
function expmod(base, exp, m) {
if(exp === 0)
return 1;
if(is_even(exp)){
// We are in a conditional statement, so it's safe to call:
const half_exp = expmod(base, exp / 2, m)
return half_exp * half_exp % m
}
return base * expmod(base, exp - 1, m) % m;
}
console.log(expmod(4, 3, 5)) //4
Комментарии:
1. глубоко признателен за терпеливую проработку, спасибо.
Ответ №3:
Я просто не могу понять, когда
exp === 0
он завершается,1
но почему выполняется half_exp?
Нет, когда exp
0
выражение не завершается 1
. exp === 0 ? 1: is_even(exp)
будет вычисляться 1
, и все выражение будет действовать как условие для следующего троичного оператора. Поскольку 1
это значение trucy, поэтому все выражение будет вычисляться как half_exp * half_exp % m
//Initial expression
(exp === 0 ? 1
: is_even(exp)) ? (half_exp * half_exp % m)
: (base * expmod(base, exp - 1, m) % m);
//Put exp = 1
(1 === 0 ? 1
: is_even(exp)) ? (half_exp * half_exp % m)
: (base * expmod(base, exp - 1, m) % m);
//Evaluate the first ternary expression
(1) ? (half_exp * half_exp % m)
: (base * expmod(base, exp - 1, m) % m);
//The result of first will convert to boolean
Boolean(1) ? (half_exp * half_exp % m)
: (base * expmod(base, exp - 1, m) % m);
//The result of first expression acts as condition for second ternary operator
true ? (half_exp * half_exp % m)
: (base * expmod(base, exp - 1, m) % m);
//As condition is true to first expression is returned
(half_exp * half_exp % m)
Комментарии:
1.
true ? 1 : false ? 0 : 2
вычисляется какtrue ? 1 : (false ? 0 : 2)
, а не как(true ? 1 : false) ? 0 : 2
. Поэтому я не думаю, что следующее предложение является правильным: »exp === 0 ? 1: is_even(exp)
будет вычисляться1
, и все выражение будет действовать как условие для следующего троичного оператора». Вы можете проверить это, выполнив:true ? 1 : false ? 0 : 2 //=> 1
2. тест с
true ? 1 : false ? 0 : 2 //=> 1
, ty.@3limin4t0r этого не произойдет.