ESPNBarnwell on five important Week 3 losses and their aftermathThe Jerusalem PostIranians stagger under soaring cost of living after seven months of war with the USESPN DeportesAutoridades de Manchester City rebaten veredicto de Premier LeagueBollywood HungamaGondhal team meets Maharashtra CM Devendra Fadnavis, appeals for financial support ahead of Oscar 2027 journeyDaily MaverickStudent protests and French public sector strike heap pressure on MacronRTP DesportoSalvador diz que Sporting de Braga "exige mais" do que o que foi apresentadoPunchMeet Nigerian dancer attempting 168-hour Guinness World RecordBillboardElton John Opens London’s Apple Music Hall With Intimate, Hit-Packed PerformanceVariety‘GMA’ Anchor and Christopher Reeve’s Son Will Reveals Testicular Cancer Diagnosis: ‘I Was Raised to Share One’s Struggles’Egypt IndependentBritain’s PM is on a ‘Burnham bounce.’ When will he fall to earth?Antara NewsJakarta rises to 140th worldwide in Global Cities Index 2026Il Fatto Quotidiano“Mostrava i genitali e si masturbava di fronte a una 18enne”: giudice sospeso dal Csm. Lui: “Solo frasi volgari, mi scuso”
The Daily Newsstand · Free, Always
Tuesday, September 29, 2026

Как слизень ищет укрытие: от наивного решения к стратегии

Translate

Наивность, fail‑fast, fallback, failback, оптимизация и стратегия — на слизне, на кухне и на JavaScript

Эти слова программисты произносят каждый день, и почти каждый понимает их немного по‑своему. Можно было бы начать со словаря: шесть терминов, шесть определений. Это был бы наивный план — в том самом смысле, о котором пойдёт речь ниже, — и к концу статьи станет видно, почему я от него отказался.

Вместо словаря будет задача. Понятия появляются по одному — тогда, когда без них уже нельзя двигаться дальше. Каждое получает образ, точное определение и код.

Как читать. Статья написана сразу для трёх читателей.

  • Если вы не программист, код можно пропускать: всё важное из него сказано словами рядом.

  • Если вы математик, ищите блоки «Строже»: там формулировки без аналогий. Остальные могут их пропускать.

  • Если вы программист: весь код на JavaScript без зависимостей, любой фрагмент запускается в консоли браузера или в Node.js.

По ходу я задаю вопросы. Они не риторические: если вы нашли ответ раньше, чем дочитали до него, — хороший знак.

1. Слизень под солнцем

Утро, солнце поднимается. Для слизня это опасность: на солнце он теряет влагу и в конце концов высыхает. Ему нужно укрытие — и чем раньше, тем лучше.

Рядом три углубления:

  • А — широкая ямка, вход прямо на поверхности. Добраться легко, но она мелкая: когда солнце поднимется выше, оно достанет до дна.

  • Б — узкая трубка, уходящая вниз под углом.

  • В — узкая вертикальная трубка. Из неё тянет сыростью.

Насколько глубоки трубки, неизвестно. Внутри темно, и слизень там ничего не видит. Он умеет ползти, изгибаться, протискиваться и пятиться, но физику не нарушает: слишком узкий проход он не пройдёт, слишком высокий уступ не преодолеет.

Главная трудность видна уже здесь. Чтобы узнать, годится ли трубка, в неё нужно залезть. А залезть — значит потратить время, которое идёт под солнцем. Исследование расходует тот же ресурс, ради сохранения которого мы его затеяли. Всё, что будет дальше, — способы жить с этим противоречием.

Наш слизень мысленный: он рассуждает. Настоящие слизни решают эту задачу инстинктом, и мы ещё вернёмся к тому, откуда инстинкт знает ответ.

2. Наивность: сначала — решение, которое точно работает

Что значит «найти укрытие»? Скажем так: укрытие — это место, куда слизень помещается и куда не достаёт солнце.

Самое прямое решение следует из этого определения буквально. Взять первое углубление, заползти до конца, проверить оба условия. Не подошло — выбраться и взять следующее. Это полный перебор. Он ничего не знает о трубках, ничего не предполагает и ничего не угадывает. И он работает: если укрытие есть, перебор его найдёт.

При одном условии — если хватит времени. Солнце в определении укрытия не участвовало, и перебор о нём не знает.

Наивная реализация — прямой перевод определения в действия, без какого‑либо знания о задаче сверх самого определения.

Слово «наивный» здесь не ругательство. Наивное решение не глупое — оно доверчивое: верит определению и ничему больше. Поэтому оно работает везде, где работает определение, и ровно так, как сказано в определении.

Заметьте: «ползти в ближайшую» — это уже не наивность. Это жадное правило, эвристика: знание (или вера), что ближайшее обычно и есть лучшее. Жадное правило быстрее перебора, но может завести в тупик; перебор — нет. Простое и наивное — разные вещи.

Наивное возведение в степень

То же самое на числах. Что такое 2⁵? По определению — пять умножений на два:

2⁵ = 1 · 2 · 2 · 2 · 2 · 2

Показатель — это количество умножений. Наивная реализация повторяет это дословно:

function powNaive(n, x) {
  let result = 1;               // ещё ни одного умножения
  for (let i = 0; i < x; i++) {
    result *= n;                // одно умножение на каждую единицу показателя
  }
  return result;
}

powNaive(2, 10);  // 1024
powNaive(34, 0);  // 1

Ни таблиц, ни формул, ни битовых трюков. Умножений ровно x.

Посмотрите на powNaive(34, 0). Цикл не выполнился ни разу, и осталось то, с чего начали, — единица. Правило «любое число в нулевой степени равно 1», которое в школе заучивают, здесь никто не программировал. Оно получилось само.

Один факт, три взгляда:

  • Бытовой: ни разу не умножали — значит, ничего не изменилось. А «ничего не изменилось» для умножения — это «умножили на 1».

  • Программистский: начальное значение аккумулятора. Пустой цикл возвращает то, с чего начал.

  • Математический — в блоке ниже.

Строже. Произведение нуля сомножителей (пустое произведение) равно нейтральному элементу умножения, то есть 1, — так же как пустая сумма равна 0. К тому же выводу ведёт правило, которое степень обязана сохранять: 2^a · 2^b = 2^(a+b). При a = 0 получаем 2⁰ · 2^b = 2^b, откуда 2⁰ = 1. Это не исключение из правила, а его следствие.

А что наивная реализация вернёт для 0⁰? Тоже 1: цикл снова не выполнился. Математики же договариваются об этом случае по‑разному в разных областях. В алгебре и комбинаторике 0⁰ = 1: отобразить пустое множество в пустое можно ровно одним способом. В анализе 0⁰ — неопределённость: предел f(x)^g(x), где обе функции стремятся к нулю, может оказаться разным. Наивная реализация молча выбрала сторону. JavaScript выбрал ту же: 0 ** 0 === 1.

Зачем нужна наивная реализация

  1. Пол. Она работает всегда, когда работает определение. Если всё остальное сломалось, на неё можно опереться.

  2. Эталон. Любую хитрую версию проверяют сравнением с наивной: там, где определены обе, ответы обязаны совпасть. В тестировании такую эталонную реализацию называют оракулом.

  3. Карта границ. Наивная реализация показывает, где кончается определение. Об этом — следующий раздел.

3. Граница модели и fail‑fast

Вопрос: что вернёт powNaive(2, -2)? А powNaive(2, 2.5)?

powNaive(2, -2);   // 1 — а 2⁻² = 0.25
powNaive(2, 2.5);  // 8 — а 2^2.5 ≈ 5.657
powNaive(2, NaN);  // 1 — «не число» в степени дало единицу

А powNaive(2, Infinity) не вернёт ничего: условие i < Infinity верно всегда, и программа зависнет.

Ни одной ошибки. Функция уверенно отвечает — и отвечает неправду. Это хуже падения. Падение видно сразу, а неверное число уходит дальше, в другие расчёты, и обнаруживается (если обнаруживается) далеко от места, где возникло.

Причина — не баг. Код делает ровно то, что написано: умножает, пока i < x. Причина в том, что модель «показатель — это количество умножений» имеет смысл только для целых неотрицательных x. Нельзя умножить «минус два раза» или «два с половиной раза». За пределами своей модели наивная реализация не ошибается — она перестаёт что‑либо значить, продолжая выдавать числа.

У любой модели есть граница. Вопрос в том, как узнать, что мы её пересекли, — и как узнать это рано.

Частичный заход

Вернёмся к слизню. Перебор тратит время на то, чтобы заползти в трубку до конца. Но если трубка сужается уже через пару сантиметров, дальше лезть незачем: ответ «не подходит» уже известен. Значит, лезть целиком не нужно. Можно войти частично: голова внутри, хвост снаружи.

Это одновременно действие и исследование. Одна часть тела получает информацию о трубке, другая остаётся в известном месте, и отступить можно быстро. Слизень не просто быстрее узнаёт плохие новости — он устроил исследование так, чтобы плохие новости приходили первыми.

Fail‑fast — способ устроить проверку так, чтобы непригодность текущего подхода обнаруживалась как можно раньше, как можно явнее и за как можно меньшее число шагов — и однозначно говорила: здесь нужен другой подход.

Главное слово в этом определении — не «ошибка», а «устроить». Fail‑fast — не реакция («что‑то пошло не так — остановись»), а проектирование: мы заранее выбираем, что проверять и где поставить проверку, чтобы провал стал виден до того, как на неудачный путь потрачено много.

Для возведения в степень это одна строка — охранное условие (guard clause) в начале функции:

function powNaive(n, x) {
  // Fail-fast: модель «x — количество умножений» имеет смысл
  // только для целых x ≥ 0. Всё остальное — не к ней.
  if (!Number.isInteger(x) || x < 0) {
    throw new RangeError(`Показатель должен быть целым и не меньше нуля, получено: ${x}`);
  }

  let result = 1;
  
  for (let i = 0; i < x; i++) {
    result *= n;
  }

  return result;
}

Проверка ничего не чинит: считать 2⁻² функция так и не научилась. Но теперь она честно говорит «это не ко мне» вместо того, чтобы молча отвечать 1. Граница модели стала видимой. NaN и Infinity отсекаются той же строкой: целыми числами они не являются.

Проверка бывает и косвенной. Ниже, в быстрой версии, показатель будет переводиться в BigInt, и BigInt(2.5) сам бросит RangeError: дробное число нельзя превратить в целое. Язык сделает часть fail‑fast за нас. Но только часть: BigInt(-2) пройдёт без возражений.

Канарейка и дым

Шахтёры когда‑то брали с собой в забой канарейку. Птица чувствительнее человека к угарному газу и погибает раньше, чем люди что‑то заметят. Канарейка — живой fail‑fast: дешёвый датчик, который срабатывает, пока проблема ещё не смертельна. Отсюда канареечный релиз: новую версию программы сначала выкатывают на малую долю пользователей и смотрят, не станет ли им хуже.

Ещё проще smoke test — проверка «на дым». Собрал устройство, включил: пошёл дым — дальше тестировать нечего. Самая короткая проверка из возможных: один шаг, однозначный ответ.

Раньше — не значит дёшево

Из слов «как можно раньше» легко вывести, что fail‑fast — всегда дешёвая проверка. Это не так. Раньше — значит до того, как на неудачный путь уйдёт больше, чем стоит сама проверка.

Частичный заход слизня — не взгляд со стороны, а полноценное действие: время, силы, риск. Посмотреть на вход снаружи дешевле, но это ничего не решает: сужение может начаться глубже. Канареечный релиз требует инфраструктуры и рискует теми, на ком проверяют. Повар, который пробует соус до того, как подать его двадцати гостям, тратит время и соус. Все три проверки дорогие — и все оправданы, потому что стоят на сюжетной развилке: их ответ решает судьбу целой ветви, а не одного шага.

Второе оправдание дорогой проверки — переиспользование. Набор автотестов дорого написать, но он годами проверяет каждое изменение кода: его цена раскладывается (амортизируется) на тысячи запусков. Проверка, ставшая частью повторяемой стратегии, окупается не одним спасённым случаем, а всеми.

Отмерить или попробовать: LBYL и EAFP

В сообществе Python закрепились названия для двух стилей проверки.

  • LBYL — Look Before You Leap, “посмотри, прежде чем прыгать”: сначала проверь, потом действуй. По‑русски — «семь раз отмерь, один раз отрежь». Слизень ощупывает вход рожками.

  • EAFP — Easier to Ask Forgiveness than Permission, “проще попросить прощения, чем разрешения” (фразу приписывают Грейс Хоппер): сначала действуй, провал обработай. Слизень лезет внутрь и смотрит, что будет.

LBYL дешевле, но видит только то, что доступно снаружи: ширину входа, а не сужение на глубине. EAFP дороже, но проверяет саму правду, а не её признаки. У LBYL в программировании есть ещё одна слабость: между проверкой и действием мир успевает измениться. Файл существовал в момент проверки и исчез к моменту открытия. Поэтому некоторые вещи честно проверяются только попыткой.

Fail‑fast — не третий стиль рядом с этими двумя, а другая ось. LBYL и EAFP отвечают на вопрос как обнаружить проблему: заранее или попыткой. Fail‑fast — на вопрос когда: как можно раньше. Охранное условие в powNaive — fail‑fast в стиле LBYL. Частичный заход слизня — fail‑fast в стиле EAFP.

У проверки две ошибки

Любая проверка ошибается двумя способами: пропускает плохое или бракует хорошее. Канарейка может погибнуть от холода, а не от газа, и шахтёры зря покинут забой. Слишком мягкий fail‑fast бесполезен, слишком строгий отсекает рабочие варианты. Ложная тревога — тоже цена проверки, и её тоже взвешивают. (В разделе 8 будет функция, в которую я намеренно оставил такую ложную тревогу. Попробуйте заметить её раньше, чем дойдёте до разбора.)

Отступить — не значит вернуться в начало

Слизень залез в трубку Б на полтела, почувствовал сужение и выбрался. Физически он почти там же, где был. Но задача уже другая. Он знает, что Б сужается примерно на длине его тела; что до сужения там сыро и прохладно; что вариантов стало на один меньше.

Неудачная попытка — не потерянное время, если её результат сохранён. Ценность fail‑fast не только в том, что отказ наступает рано. После раннего отказа остаются силы, время и знание, чтобы ими воспользоваться. Как именно — следующий раздел.

4. Fallback и failback: вниз и обратно

Трубка Б не подошла. Самая перспективная — В: вертикальная, сырая, прохладная. Но в ней сидит жук. Остаётся ямка А: мелкая, но прямо сейчас, пока солнце низко, в ней есть тень. Слизень забирается в А.

Это fallback — переход к запасному плану. Запасной план хуже основного, но у него есть решающее достоинство: он держится на меньшем числе допущений. Трубке В нужно быть свободной, достаточно широкой и глубокой. Ямке А нужно только, чтобы солнце ещё не поднялось высоко.

Через полчаса жук уползает, и слизень перебирается в В. Это failback — возвращение к предпочтительному плану, когда его допущения снова выполнились.

Термины пришли из инженерии отказоустойчивых систем. Когда основной сервер падает, нагрузку переключают на резервный — это failover. Когда основной восстановлен, нагрузку возвращают на него — это failback. Fallback — слово более общее: любой запасной вариант, на который переходят, когда основной не сработал.

Все три знакомы любому, кто смотрел видео через плохой интернет. Сеть просела — плеер снижает качество с 1080p до 360p, но не останавливается: fallback. Сеть восстановилась — качество возвращается: failback. Плеер выбирает не между «идеально» и «ничего»: у него есть лестница вариантов, и он ходит по ней в обе стороны. Такое поведение называют плавной деградацией (graceful degradation).

Fallback — переход к запасному плану, который держится на меньшем числе допущений. Цепочка fallback’ов спускается ступенька за ступенькой, и нижняя ступенька — наивное решение: у него допущений меньше всего.

Failback — возвращение к предпочтительному плану, когда его допущения снова выполняются.

Fallback прост: что‑то сломалось — шаг вниз. Failback устроен хитрее. Чтобы вернуться, нужно три вещи.

  1. Помнить, куда возвращаться. Слизень, забывший, что В лучше, так и останется в А, пока солнце не доберётся до дна. Стратегия, которая умеет только отступать, со временем оказывается на нижней ступеньке — даже когда проблемы давно нет.

  2. Проверить, что условия действительно восстановились. Жук мог уползти на минуту. Прежде чем переползать целиком, слизень снова заглядывает в В: опять частичный заход, опять fail‑fast. Только теперь он проверяет не «можно ли туда», а «можно ли обратно».

  3. Не метаться. Если жук то уползает, то возвращается, слизень, реагирующий на каждое движение, будет всё время в пути — и всё время под солнцем. Нужен порог: возвращаться, только если В свободна достаточно долго.

Третий пункт хорошо знаком по термостату. Если отопление включается ниже 20° и выключается выше 20°, котёл будет щёлкать без конца. Поэтому его включают ниже 19° и выключают выше 21°. Этот зазор называется гистерезисом, и он нужен любому failback’у, чтобы система не дрожала на границе.

Автомат в щитке

В электрощитке любой квартиры стоит автомат. При перегрузке он размыкает цепь сразу, не дожидаясь, пока расплавится проводка, — это fail‑fast. Когда причину устранили, автомат включают обратно — это failback.

Программисты взяли эту идею целиком и назвали так же — circuit breaker. Допустим, программа берёт курс валют с сервера. Если сервер лёг, каждый запрос к нему будет долго ждать и падать. Лучше после нескольких неудач перестать обращаться к серверу, отдавать последний сохранённый курс и время от времени осторожно проверять, не поднялся ли сервер.

function createBreaker(primary, fallback, { maxFailures = 3, retryAfterMs = 5000 } = {}) {
  let failures = 0;
  let openedAt = null; // когда перестали доверять основному плану

  return async function call(...args) {
    const isOpen = openedAt !== null;
    const isProbe = isOpen && Date.now() - openedAt >= retryAfterMs;

    if (isOpen && !isProbe) {
      return fallback(...args);   // fail-fast: ответ уже известен, даже не пытаемся
    }
    try {
      const result = await primary(...args);
      failures = 0;
      openedAt = null;            // failback: основной план снова работает
      return result;
    } catch {
      failures++;
      if (isProbe || failures >= maxFailures) {
        openedAt = Date.now();    // порог пройден: перестаём обращаться к основному
      }
      return fallback(...args);   // fallback: запасной план
    }
  };
}

Попробовать можно так:

let serverUp = false;
const fetchRate = async () => {
  if (!serverUp) throw new Error('сервер недоступен');
  return 'свежий курс';
};
const getRate = createBreaker(fetchRate, () => 'курс из кэша', { retryAfterMs: 1000 });

await getRate();  // 'курс из кэша' — первая неудача
await getRate();  // 'курс из кэша' — вторая
await getRate();  // 'курс из кэша' — третья: автомат разомкнулся
await getRate();  // 'курс из кэша' — к серверу даже не обращались
serverUp = true;
await getRate();  // (через секунду) 'свежий курс' — пробный запрос удался

В двадцати строках все три понятия. Fallback — если основной план не сработал, отдаём запасной, кэш. Fail‑fast — пока автомат разомкнут, к серверу даже не обращаемся: мы уже знаем, чем это кончится, и не тратим на это время. Failback — раз в retryAfterMs пробный запрос; удался — возвращаемся к основному плану. И гистерезис дважды: автомат размыкается не после первой неудачи, а после maxFailures, и пробует вернуться не сразу, а через retryAfterMs.

Проверка и отступление живут в разных местах

Fail‑fast и fallback работают парой. Проверка без отступления — честная, но бесполезная остановка. Отступление без проверки включается слишком поздно, когда ущерб уже нанесён.

При этом живут они обычно в разных местах. powNaive знает свою границу, но не знает, что делать за ней, — и просто бросает ошибку. Что делать дальше, решает тот, кто её вызвал: он знает, какие есть альтернативы. Проверка живёт там, где известно допущение; решение об отступлении — там, где известны альтернативы. Ради такого разделения в языках программирования и существуют исключения.

5. Оптимизация: знание в обмен на общность

Начнём с самой маленькой оптимизации из возможных. В powNaive первое умножение всегда 1 · n. Его можно сэкономить: начать сразу с n и сделать на одно умножение меньше.

function powMinusOne(n, x) {
  let result = n;                 // сразу n, без умножения на 1
  for (let i = 1; i < x; i++) {
    result *= n;
  }
  return result;
}

powMinusOne(2, 3);  // 8 — верно, и на одно умножение меньше
powMinusOne(2, 0);  // 2 — а должно быть 1

Сэкономленное умножение стоило нам случая x = 0. Функция молча предположила, что x ≥ 1, и за пределами этого допущения врёт. Чтобы починить её, придётся добавить особый случай if (x === 0) return 1 — то есть отдельно проверить границу, которую мы сами же и сдвинули.

Это вся оптимизация в миниатюре. Мы добавили знание (x ≥ 1), получили выигрыш (минус одно умножение) и заплатили общностью: модель стала уже, и её границу теперь нужно охранять. В наивной версии ноль обрабатывался сам собой. В оптимизированной — уже нет.

Быстрое возведение в степень

Выигрыш в одно умножение смешон. Но если посмотреть на задачу внимательнее, можно выиграть почти всё.

Наивно x⁸ — это восемь умножений. Но x⁸ = (x⁴)², x⁴ = (x²)², x² = x · x. Три возведения в квадрат — и готово.

А если показатель не степень двойки? Возьмём 13. В двоичной записи 13 = 1101₂, то есть 8 + 4 + 1, а значит x¹³ = x⁸ · x⁴ · x. Будем по очереди получать x, x², x⁴, x⁸ — каждое как квадрат предыдущего — и домножать результат только на те, которым в двоичной записи показателя соответствует единица:

Шаг

Остаток показателя

Младший бит

Результат

Текущий квадрат

1

1101

1 — берём

x

x → x²

2

110

0 — пропускаем

x

x² → x⁴

3

11

1 — берём

x⁵

x⁴ → x⁸

4

1

1 — берём

x¹³

x⁸ → x¹⁶

Семь умножений вместо тринадцати. В коде:

function powFast(n, x) {
  // Показатель переводим в BigInt: в JavaScript побитовые операции над обычными
  // числами (Number) работают только с 32 битами. Это особенность языка, а не алгоритма.
  let e = BigInt(x);
  let result = 1;
  let square = n;                 // по очереди: n, n², n⁴, n⁸, ...

  while (e > 0n) {
    if (e & 1n) {
      result *= square;           // младший бит равен 1 — эта степень входит в ответ
    }
    square *= square;             // n^k → n^(2k)
    e >>= 1n;                     // отбрасываем младший бит: 1101 → 110
  }
  return result;
}

Для x¹⁰⁰⁰ наивная версия делает 1000 умножений, быстрая — 16. Для показателя в миллиард: миллиард против 43.

Здесь и появляются битовые операции: e & 1n читает младший бит, e >>= 1n отбрасывает его. Двоичная запись числа перестала быть просто способом его хранить — она стала планом вычисления.

Быструю версию проверяем наивной — вот и пригодился оракул:

// Наивная версия — эталон: где определены обе, ответы обязаны совпасть.
// Диапазон выбран так, чтобы все результаты Number представлял точно.
for (let n = 0; n <= 9; n++) {
  for (let x = 0; x <= 16; x++) {
    if (powFast(n, x) !== powNaive(n, x)) {
      throw new Error(`Расхождение: ${n}^${x}`);
    }
  }
}
console.log('Быстрая версия совпала с наивной');

Во что это обошлось

Быстрая версия сложнее: наивную можно проверить глазами, быструю уже приходится проверять тестом. И она опирается на допущения, которых у наивной не было.

Первое очевидно: показатель — целый и неотрицательный. Второе спрятано глубже. Мы переставили скобки: наивная версия считает ((1 · x) · x) · x…, быстрая — (x · x) · (x · x)… Это законно, только если от расстановки скобок результат не зависит, то есть если умножение ассоциативно. Для целых чисел — да. Для чисел с плавающей точкой — лишь приблизительно: каждое умножение округляет, и за пределами точного диапазона быстрая и наивная версии начинают расходиться в последних знаках. Поэтому оракул и проверяет только точный диапазон.

Строже. Быстрому возведению в степень нужна только ассоциативность операции (полугруппа, а с единицей — моноид). Поэтому тот же алгоритм без изменений возводит в степень матрицы, перестановки, вычеты по модулю; на возведении в степень по модулю держится, например, RSA. Оптимизация, чьё допущение названо точно, переносится далеко за пределы исходной задачи: не «числа», а «любая ассоциативная операция».

И ещё: быстрый — не значит наилучший. Двоичный метод тратит на x¹⁵ шесть умножений (x², x⁴, x⁸ и три домножения; реализация выше ради простоты делает ещё пару лишних). А можно за пять: x², x³ = x² · x, x⁶ = (x³)², x¹² = (x⁶)², x¹⁵ = x¹² · x³. Поиск кратчайшей такой аддитивной цепочки — трудная задача, и ради каждого показателя её никто не решает. Быстрый алгоритм не оптимален. Он достаточно хорош.

Оптимизация стареет

Самая знаменитая оптимизация этого рода — быстрый обратный квадратный корень из Quake III Arena (1999). Для освещения в 3D‑графике нужно постоянно считать 1/√x, а деление и корень тогда стоили дорого. Код брал битовое представление числа с плавающей точкой, трактовал его как целое, вычитал его из магической константы 0x5f3759df и получал грубое приближение, которое затем уточнял одним шагом метода Ньютона.

Допущения этого хака лежали глубже любой формулы: точный формат 32-битного числа с плавающей точкой (IEEE 754), то, что целочисленные операции дешевле деления, и то, что игре хватит точности в доли процента. Эти допущения постарели: современные процессоры умеют считать приближённый обратный корень отдельной инструкцией, и хак обычно больше не выигрывает.

У оптимизации есть срок годности: она верна, пока верны её допущения о мире. Наивная версия не стареет — её единственное допущение само определение.

Цена поиска

У слизня тоже есть оптимизация — и тоже в обмен на допущение. Из трубки В тянет сыростью. Если сырость говорит о глубине — а обычно говорит, — В разумно проверить первой. Это предсказание: вывод о невидимой части пространства по видимым признакам. Оно экономит время, пока верно. Но сырой может оказаться и мелкая лужица после дождя. Поэтому предсказание не отменяет частичного захода, а только решает, с какой трубки его начинать.

По мере движения предсказание уточняется. Трубка уже полметра идёт вниз — вероятно, пойдёт и дальше. Стенки сближаются — дальше, скорее всего, будет ещё уже. Наблюдение, предположение, действие, новое наблюдение.

Теперь другой вопрос: когда остановиться? Слизень нашёл трубку, где солнце его не достанет. Может, где‑то есть прохладнее и просторнее. Искать дальше или остаться?

Ответ зависит не столько от того, насколько хороша найденная трубка, сколько от того, сколько стоит продолжение поиска. Если солнце вот‑вот доберётся — оставаться. Если утро раннее — можно поискать. Герберт Саймон назвал такой подход satisficing (satisfy + suffice): искать не наилучшее решение, а достаточно хорошее, потому что поиск наилучшего сам стоит ресурсов. В теории принятия решений та же развилка называется «исследование или использование» (explore/exploit): продолжать узнавать новое или пользоваться тем, что уже знаешь.

Строже. Иногда момент остановки можно вычислить. В «задаче о разборчивой невесте» (secretary problem) кандидатов смотрят по одному, и к отвергнутому вернуться нельзя. Оптимальная стратегия — пропустить первые n/e ≈ 37% кандидатов, запомнив лучшего из них, а затем взять первого, кто окажется лучше. Вероятность выбрать лучшего стремится к 1/e ≈ 37%. Слизню проще: он может вернуться к отвергнутой трубке. Именно поэтому failback так ценен — возможность возврата меняет всю математику остановки.

«Преждевременная оптимизация — корень всех зол», — писал Дональд Кнут. Фразу обычно цитируют как запрет, но смысл в другом. Оптимизация — покупка, и платить за неё стоит, когда известно, что покупаешь. Пока неизвестно, где программа на самом деле тратит время, каждое допущение, введённое ради скорости, — это долг без гарантии, что он окупится.

Оптимизация — улучшение решения по выбранному критерию за счёт дополнительного знания о задаче. Это знание входит в решение в виде допущений, и за него платят общностью, простотой и новыми проверками. Оптимизация — движение к лучшему, а не обязательно достижение наилучшего.

Ключевое слово здесь — критерий. «Быстрее», «точнее», «понятнее», «меньше памяти» — разные критерии, и улучшение по одному часто ухудшает другой. Вопрос «какой вариант лучше?» без критерия ответа не имеет. А выбор критерия — это уже не оптимизация. Это стратегия.

6. Стратегия: работа, которая происходит до плана

Слово пришло из греческого: стратег (στρατηγός) — полководец, буквально «ведущий войско». Полководец сам не сражается. Его работа — решить, какое сражение давать, где, когда, какими силами и куда отходить, если оно пойдёт плохо. Сражается армия; полководец выбирает, по какому плану.

Вечер, холодильник, гости

Вы пришли домой голодным. Решение кажется мгновенным: «сделаю омлет». Проследим, что будет дальше.

Вы открываете холодильник: два яйца. На омлет для одного хватит. Звонит друг: будет через час, и не один. Теперь нужно накормить троих, и не через десять минут, а через час. Омлет из двух яиц на троих не годится, зато появился час: можно запечь курицу, это минут пятьдесят. Вы включаете духовку — она не греется. Остаются паста (макароны есть, соус соберётся из того, что в холодильнике) или магазин за углом, где есть всё, но это ещё двадцать минут. Вы пишете друзьям, что будет паста, — и узнаёте, что один из них не ест глютен.

Ничего особенного не произошло: обычный вечер. Но за эти четверть часа вы проделали работу, которую стоит разобрать по частям, потому что это и есть стратегическое мышление.

  1. Назвать цель и критерий. «Поесть самому» и «накормить троих» — разные задачи с разными мерами успеха: сытость через пятнадцать минут против «всем вкусно, никто не сидит голодным». Звонок друга не добавил работы — он заменил задачу. Пока критерий не назван, варианты нельзя сравнить: неясно, что значит «лучше».

  2. Перечислить репертуар. Какие планы вообще доступны: омлет, паста, курица, бутерброды, доставка. Стратегия не придумывает планы из ничего — она выбирает из того, что вы умеете. Умение готовить одно блюдо — это план. Умение готовить десять — материал для стратегии.

  3. Вскрыть допущения каждого плана. Омлет — если есть яйца. Курица — если работает духовка и есть пятьдесят минут. Доставка — если есть деньги и час на ожидание. Паста — если все едят глютен. План — это действия плюс список «если». Этот список обычно никто не пишет, потому что он кажется очевидным. Но именно в нём живут все будущие провалы.

  4. Выбрать факторы. Какие величины действительно меняют исход? Время, продукты, число людей, ограничения в еде. А внешний вид блюда — фактор? Для ужина с друзьями, возможно, нет; для первого свидания — возможно, да. Факторы — не свойство мира, а взгляд на него: гипотеза о том, что важно. Если гипотеза неверна, стратегия будет уверенно решать не ту задачу. Вы не считали диету фактором — и узнали о ней последней.

  5. Расставить проверки. Заглянуть в холодильник — дёшево и сразу отсекает половину вариантов (LBYL). Работает ли духовка, можно узнать, только включив её (EAFP). Сначала — дешёвые и решающие проверки, потом дорогие; решающие ставят на развилки, до того как на ветку уйдут силы. Это fail‑fast, только теперь он работает на масштабе всего вечера, а не одного шага.

  6. Построить лестницу отступлений. Курица → паста → бутерброды. Каждая ступенька ниже держится на меньшем числе допущений. Нижняя, бутерброды, — наивное решение: хлеб и что‑нибудь сверху, почти без условий, работает всегда. Лестницу строят до того, как основной план сломался: fallback, придуманный в момент провала, обычно хуже продуманного заранее.

  7. Оценить цену недостающего. Магазин — не ступенька вниз, а способ купить допущение. Нет безглютеновой пасты — можно за ней сходить. Двадцать минут дороги против гостя, оставшегося без ужина. Выбор между «приготовить из того, что есть» и «сходить за недостающим» — это оптимизация внутри стратегии: окупит ли выигрыш свою цену.

  8. Назначить условия возврата. Возможно, духовка просто медленно разгорается. Если она нагреется в ближайшие десять минут, курица успеет — это failback. Условие возврата стоит назвать заранее и с порогом: «нагреется в течение десяти минут — курица, иначе окончательно паста». Без порога можно весь вечер переключаться между планами и не приготовить ничего.

  9. Решить, сколько думать. Если вы так голодны, что думать не получается, съешьте кусок хлеба прямо сейчас. Это не отказ от стратегии, а стратегический ход: купить время. Давление спадает — и появляется возможность выбрать план получше. Слизень делает то же, забираясь в мелкую ямку А: это не решение задачи, а передышка для её решения. Стратегия включает и решение о том, сколько ресурсов тратить на саму стратегию.

  10. Разобрать итог. После ужина вы знаете больше, чем до него: про духовку, про диету друга, про то, что пасты на троих нужно больше, чем казалось. В следующий раз вы спросите про ограничения в еде до выбора блюда. Поздняя неприятная находка превратилась в раннюю дешёвую проверку.

Это не список шагов

Если перечитать вечер, видно, что шаги шли не по порядку. Звонок вернул к пункту 1: поменялась цель. Сломанная духовка — к пункту 6. Диета друга — к пункту 4: оказалось, что неверно выбраны сами факторы. Стратегическая работа — не конвейер, а петля, которая то и дело откатывается назад. Эти «танцы с бубном» — не беспорядок, а нормальный ход работы в мире, который раскрывается по частям.

Два вида откатов стоит различать. Когда проверка говорит «этот план не сработает», меняют план: омлет → паста. Когда проверка говорит «ты неправильно понимаешь задачу», меняют модель: критерий, факторы, репертуар. Теоретик организаций Крис Аргирис называл это одинарной и двойной петлёй обучения. Первая — fail‑fast для действий, вторая — fail‑fast для взглядов. Стратегия, у которой есть только первая петля, будет быстро и уверенно перебирать планы для не той задачи.

Вкус

Опытный повар не проходит все десять пунктов. Он открывает холодильник и уже знает, что готовить. Кажется, это чутьё. Но чутьё — не магия, а сжатый итог сотен таких вечеров. Каждый прошлый разбор переносил какую‑то проверку из конца в начало, и со временем почти все проверки оказались в самом начале — настолько быстрыми, что их уже не замечают. Программист сказал бы: вкус — это кэш стратегической работы.

Этот кэш не обязательно наработан лично. Бабушкин рецепт — план, проверенный поколениями. Кухня народа — уже стратегия: какие блюда для какого сезона, повода и достатка. Инстинкт слизня — стратегия, выработанная естественным отбором: слизни, которых тянуло к тени и сырости, чаще доживали до потомства. У программистов то же самое называется паттернами, «лучшими практиками» и разборами инцидентов — сжатым опытом отрасли. Во всех случаях это усреднённый итог множества попыток, проделанных кем‑то до вас.

У кэша есть известная беда: он устаревает. Инвалидация кэша, как шутят программисты, — одна из двух по‑настоящему трудных вещей в их профессии. Бабушкин рецепт рассчитан на её печь, инстинкт слизня — на мир без раскалённого асфальта, лучшие практики — на прошлое поколение техники, как хак из Quake. Вкусу тоже нужен fail‑fast: время от времени проверять, верны ли ещё его допущения.

Определение

Теперь можно сказать, что такое стратегия, и отличить её от плана.

План — последовательность действий, которая приводит к цели, если выполнены её допущения.

Стратегия — план выбора планов. Это результат работы, которая заранее определяет, какой план брать при каких факторах, где и чем проверять его допущения, куда отступать при провале и когда возвращаться — с учётом цены самих проверок и цены промедления.

Короче: стратегия — это планы, сшитые проверками. Между проверками выполняется план; в точках проверки делается выбор. Fail‑fast ставит эти точки как можно раньше. Fallback и failback — пути, по которым из них уходят вниз и возвращаются обратно. Оптимизация улучшает выбор внутри ветки. Наивное решение — дно, ниже которого не падают.

Отсюда видно, почему стратегия — не просто «сделать хорошо и на будущее». Хороший план тоже бывает; стратегия отличается от него не качеством, а уровнем: она работает не с действиями, а с планами. Она лавирует между ними вслед за факторами — и чем изменчивее факторы, тем больше в ней нужды. В неизменном мире хватило бы одного хорошего плана. Рядом стоит тактика — как хорошо выполнить выбранный план: как нарезать лук, когда солить воду. Стратегия решает, варить ли пасту вообще.

В программировании у слова есть и узкое значение — паттерн «Стратегия» из книги «банды четырёх» Design Patterns (1994): семейство взаимозаменяемых алгоритмов с общим интерфейсом, между которыми программа выбирает во время работы. Паттерн — это розетка, в которую можно вставлять разные планы. Он отвечает на вопрос, как сделать выбор возможным. На вопрос, что и когда выбирать, он не отвечает — это и есть стратегия в полном смысле.

Строже. В теории игр стратегия игрока — полный план действий: правило, которое каждой ситуации, где ходит игрок (точнее, каждому его информационному множеству), сопоставляет действие — включая ситуации, до которых в реальной партии может и не дойти. Это не путь, а правило на всём дереве игры. В теории управления то же различие называется управлением без обратной связи (open loop) и с обратной связью (closed loop). План — это стратегия, которая игнорирует новую информацию.

7. Граф: карта, которой нет

Каждое положение слизня — вершина. Каждое возможное действие — ребро. Вместе они образуют граф. Вот его начало:

                           (под солнцем)
             ┌───────────────────┼───────────────────┐
           в А               в Б на полтела      в В на полтела
             │                   │                   │
       (тень, мелко)       <сужается?>          <свободна?>
                            да /    \ нет         да /    \ нет
                        (назад)   (глубже)    (глубже)   (в А, ждать,
                                                          проверить снова)

Вершины здесь двух видов. В круглых скобках выбирает слизень: куда ползти. В угловых выбирает мир: что окажется внутри.

  • План — один путь по графу: «в В, вглубь, остаться». Он молча предполагает, что в каждой угловой вершине мир ответит так, как нам нужно.

  • Стратегия — правило для всего графа. В каждой вершине выбора она говорит, куда идти, а в каждой вершине мира готова к любому ответу. Если оставить только рёбра, выбранные стратегией, получится не путь, а дерево: на каждый исход — свой план. Это и есть буквальный смысл «плана о плане».

  • Fail‑fast — стремление поставить угловые вершины как можно ближе к корню: узнать ответ мира до того, как ушли далеко по ветке.

  • Fallback — ребро из неудачного ответа в соседнюю, более простую ветку. Failback — ребро обратно, на ветку, которую уже покидали. Чтобы оно существовало, вершины нужно помечать: какие посещены и что там узнали. Граф без пометок — это слизень, забывший про трубку В.

Главное отличие от учебника: слизень не знает этого графа. Он строит его по ходу, и каждое движение открывает новые вершины. Поэтому исследование здесь — не подготовка к решению, а его часть.

У алгоритмов на графах нашлись готовые имена для всего, что делал слизень:

  • «Лезть в первую трубку до конца, потом во вторую» — поиск в глубину (DFS) с возвратом (backtracking).

  • «Во все трубки понемногу, потом глубже» — поиск с итеративным углублением: сначала все ветки на глубину 1, потом на 2 и так далее. Часть работы он повторяет, зато никогда не застревает в одной бесконечно длинной ветке.

  • «Начать с той, из которой тянет сыростью» — эвристика, как в алгоритме A*. Там к уже пройденной цене пути прибавляют оценку оставшейся и в первую очередь раскрывают вершины с лучшей суммой. Оценка и есть предсказание; если она никогда не завышает реальную цену, A* гарантированно находит кратчайший путь.

Строже. Такое дерево — дерево решений в «игре против природы». Стратегия — поддерево, которое содержит ровно одно исходящее ребро из каждой своей вершины выбора и все исходящие рёбра из каждой своей вершины природы. План — частный случай, в котором природа заранее «угадана».

8. Сборка: одна функция, вся схема

Соберём всё в одном месте. Настоящая функция возведения в степень должна принимать и −2, и 2.5 — всё, для чего степень вообще определена. Одной модели на всё нет. Значит, нужна стратегия, и сначала — работа до кода.

  • Цель и критерий: правильный ответ для любого показателя, где степень определена; для целых показателей — точный.

  • Репертуар моделей. Первая: «показатель — количество умножений», для целых x ≥ 0. Вторая: то же правило, продолженное назад, n⁻ˣ = 1 / nˣ — для целых x < 0 и n ≠ 0. Третья: непрерывная, nˣ = e^(x · ln n) — для любых x, но только при n > 0.

  • Факторы: какой показатель (целый или нет, какого знака) и какое основание (ноль, положительное, отрицательное).

  • Дно: если не подходит ни одна модель — честная ошибка, а не правдоподобное число.

Строже. Откуда вторая модель? Из того же правила, что дало 2⁰ = 1: 2^a · 2^b = 2^(a+b). Если оно должно выполняться и для отрицательных показателей, то 2⁻¹ · 2¹ = 2⁰ = 1, значит 2⁻¹ = 1/2. Так же (2^(1/2))² = 2 даёт 2^(1/2) = √2. Математика расширяет степень с натуральных показателей на целые, рациональные и вещественные не произволом, а требованием сохранить правило — инвариант. Каждое расширение — новая модель со своими допущениями: для дробных показателей, например, положительное основание.

function power(n, x) {
  // Fail-fast: вход, для которого не определена ни одна модель.
  if (typeof n !== 'number' || typeof x !== 'number' || Number.isNaN(n) || Number.isNaN(x)) {
    throw new TypeError(`Ожидались два числа, получено: ${n}, ${x}`);
  }

  // Модель 1: x — количество умножений. Быстрая версия, проверенная наивной.
  if (Number.isInteger(x) && x >= 0) {
    return powFast(n, x);
  }

  // Модель 2: то же правило, продолженное на отрицательные: n^(-x) = 1 / n^x.
  if (Number.isInteger(x)) {
    if (n === 0) throw new RangeError('0 в отрицательной степени не определён');
    return 1 / powFast(n, -x);
  }

  // Модель 3: непрерывная: n^x = e^(x · ln n). Требует n > 0: ln n существует только там.
  // Внимание: эта проверка охраняет модель, а не задачу. Из-за неё power(0, 0.5)
  // упадёт на дно, хотя 0^0.5 = 0. Это намеренная ложная тревога, разбор ниже.
  if (n > 0) {
    return Math.exp(x * Math.log(n));
  }

  // Дно: отказ вместо правдоподобного числа.
  throw new RangeError(`${n} ** ${x} не определено в вещественных числах`);
}

Третья модель покрывает и целые показатели. Зачем тогда первые две? Проверьте:

Math.exp(3 * Math.log(2));  // 7.999999999999998
powFast(2, 3);              // 8

Непрерывная модель общее, но для целых показателей хуже: логарифм и экспонента округляют. Стратегия выбирает не первый подходящий план, а лучший из подходящих по критерию, а критерий мы назвали заранее: для целых показателей — точный ответ. Порядок проверок в функции — лестница: от самой узкой и точной модели к всё более общим, каждая следующая требует от показателя меньше предыдущей. Под лестницей — отказ.

Ложная тревога

Обещанная ложная тревога сидит в третьей модели. power(0, 0.5) бросает «не определено в вещественных числах», хотя 0^0.5 = √0 = 0: ответ существует, и вполне обыкновенный.

Откуда в коде такая мысль? Проверка n > 0 написана с точки зрения модели. Третья модель считает степень через логарифм, а ln n существует только при n > 0: при n = 0 формула e^(x · ln n) действительно неприменима. Для модели проверка безупречна — и сработала она правильно.

Ошибка случилась на шаг позже. Проверка сказала ровно то, что и должен говорить fail‑fast: «этот подход здесь не годится, нужен другой». Но другой ступеньки в лестнице нет, и мы провалились сразу на дно. А дно говорит от имени всей задачи: «не определено». Отказ модели выдан за отказ задачи. Честным было бы сообщение «ни одна из моих моделей сюда не подходит» — а это совсем не то же самое, что «ответа не существует».

Такая ошибка появляется естественно, поэтому её стоит разобрать. Охранное условие пишут, глядя на допущения модели: они видны прямо в формуле. Граница задачи в формуле не видна. А модель почти всегда уже задачи — на то она и модель. Поэтому проверка на границе модели по умолчанию строже, чем нужно задаче, и её срабатывание — сигнал стратегии «поищи другую модель», а не приговор входу.

Другая модель здесь есть, и совсем простая: 0^x = 0 при любом x > 0. Это ещё одна ступенька перед третьей моделью. Я не добавил её намеренно: исправленная функция показала бы только результат, а неисправленная показывает, как ложная тревога рождается из совершенно правильной проверки.

Где здесь каждое понятие:

  • Наивность. powNaive не вызывается ни разу, но без неё powFast было бы нечем проверить.

  • Оптимизация. powFast вместо powNaive: допущения (целый показатель, ассоциативность) в обмен на скорость.

  • Fail‑fast. Первая проверка, отказ для нуля в отрицательной степени и BigInt внутри powFast как косвенная проверка. И цена проверок — ложная тревога из разбора выше.

  • Fallback. Здесь он превратился в предварительный выбор: проверки стоят перед каждой моделью, поэтому до провала дело не доходит. Это тот же перенос проверок из конца в начало, что и у опытного повара.

  • Failback здесь не нужен: вид показателя не меняется во время вызова. Failback нужен там, где мир меняется со временем, как в circuit breaker. Стратегия берёт только те механизмы, которых требует задача.

  • Стратегия — всё вместе: критерий, модели, факторы, порядок проверок, дно.

И вся схема одной таблицей:

Слизень

Кухня

Код

Наивность

перебор трубок до конца

бутерброды

powNaive

Fail‑fast

частичный заход

заглянуть в холодильник, попробовать соус

охранное условие; разомкнутый автомат

Fallback

в мелкую ямку А

паста вместо курицы

кэш вместо сервера

Failback

обратно в В, когда жук ушёл

курица, если духовка всё‑таки нагрелась

удачный пробный запрос

Оптимизация

начать с сырой трубки

сходить за недостающим, если окупится

powFast

Стратегия

правило для всех развилок графа

работа от цели до разбора итогов

power

9. Как устроена эта статья

В начале я пообещал объяснить, почему отказался от словаря. Теперь это можно сделать в тех же терминах, потому что эта статья — тоже стратегия.

  • Цель и критерий. Не «рассказать о шести терминах», а чтобы после чтения их можно было применить к своей задаче. Критерий — не «помню определения», а «узнаю их в новой ситуации».

  • Наивный план — словарь, шесть определений подряд. Он прямо следует из цели «объяснить термины» и работает — при допущении, что у читателя уже есть образы, к которым определения прикрепятся. У программиста они есть, у остальных — не всегда.

  • Fail‑fast. Это допущение проверяется до того, как написано хоть слово: поймёт ли человек, никогда не видевший программ, определение fail‑fast без примера? Нет. Словарь отпал на этапе плана, а не после публикации.

  • Факторы — три читателя с разным входным знанием и ограниченное внимание каждого.

  • Лестница и отступления. У каждого понятия несколько слоёв: образ, определение, затем код или строгая формулировка. Образ — нижняя ступенька: он держится почти ни на каких допущениях и работает для всех. Каждый читатель спускается до того слоя, который работает для него (fallback), а после каждого образа текст возвращается к точному определению (failback).

  • Проверки для читателя — вопросы по ходу текста. Если на вопрос не находится ответа, лучше заметить это сразу, а не в конце статьи, когда непонимание уже накопилось.

  • Оптимизация — сквозные примеры. powNaive вы прочитали один раз, а работал он на все шесть понятий: был наивностью, эталоном, картой границ, объектом fail‑fast, основой для оптимизации и частью итоговой стратегии. Один пример, использованный шесть раз, дешевле шести разных: цена его изучения амортизируется.

  • Бюджет на раздумья — то, что сознательно осталось за кадром: устройство чисел с плавающей точкой, причины 32-битных ограничений JavaScript, доказательства. Каждое тянет на отдельную статью, а здесь было бы веткой, уводящей от цели.

  • Разбор итогов — за вами. Вопросы ниже проверяют критерий: узнаёте ли вы эти понятия в новых ситуациях. Если где‑то не узнаёте, напишите в комментариях: для автора это поздняя находка, которая в следующий раз станет ранней проверкой.

Вопросы на дорогу

Для тех, кто любит проверять сам.

  1. Добавьте в power недостающую ступеньку для нуля. Почему простая замена n > 0 на n >= 0 выглядит как исправление, но им не является? Подсказка: что такая версия вернёт для power(0, -0.5) и как это согласуется с поведением второй модели?

  2. Почему (-8) ** (1/3) в JavaScript равно NaN, хотя кубический корень из −8 — это −2? Подсказка: как выглядит 1/3 в двоичной записи и что это значит для знаменателя.

  3. Возведите в степень с помощью алгоритма powFast не число, а матрицу [[1, 1], [1, 0]] (понадобится своё умножение матриц и своя «единица»). Какие числа получаются? Сколько умножений нужно, чтобы получить тысячное из них?

  4. Найдите цепочку из пяти умножений для x¹⁵. Найдите другие показатели, на которых двоичный метод не оптимален.

  5. Что станет с createBreaker при retryAfterMs = 0? А при maxFailures = 1?

  6. Когда наивная реализация — лучший окончательный выбор, а не только эталон?

  7. Солнце движется, и через час ямка А перестанет быть укрытием. Что это меняет в стратегии слизня?

Слизень не знает ни одного из этих слов. Но если он пережил полдень, у него была стратегия — выработанная не им, проверенная не на нём и сжатая до одного движения: к тени и сырости. Всё, что мы делали в этой статье, — разворачивали это движение обратно в работу, из которой оно состоит.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.