Троичные условия для поиска expmod?

#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.

Лексическая привязка: инициализатор идентификатора привязки

  1. Пусть bindingId будет строковым значением BindingIdentifier .
  2. Пусть lhs будет ResolveBinding(bindingId).
  3. Если IsAnonymousFunctionDefinition(инициализатор) имеет значение true, то

    a. Пусть значением будет NamedEvaluation инициализатора с аргументом bindingId .

  4. Ещё,

    a. Пусть rhs будет результатом вычисления инициализатора *.
    b. Пусть значение будет ? Получить значение (rhs).

  5. Возвращает 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 этого не произойдет.