Дело было не в алгоритме. Дело было в кодировке.

Несколько дней я был уверен, что в моей системе сидит глубокий, загадочный баг. Логи выходили мусором. Шаг конвертации данных выдавал файлы, которые оказывались едва заметно неправильными. Подпроцесс возвращал бессмыслицу вместо ожидаемого вывода. Я отправился на охоту за изъяном в своей логике, где-то в умной части системы.

В логике не было никакого изъяна. Баг был в том, что мой компьютер и я расходились во мнениях о том, как писать.

Баг, которого не было в коде

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

Эта непоследовательность, как я узнаю позже, — частая подсказка. Но тогда это выглядело просто так, будто моя программа была заколдована.

Что было на самом деле не так: две программы, два алфавита

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

В этом и был весь мой баг. Моя операционная система по умолчанию использовала более старую, унаследованную кодировку, тогда как инструменты и файлы, с которыми я работал, предполагали современную, почти универсальную. Всё работало, пока две таблицы случайно совпадали — а они совпадают для самых простых символов — и ломалось в тот миг, когда что-либо выходило за пределы этого пересечения. Текст никогда не портился намеренно. Его просто читали на другом языке, нежели тот, на котором он был написан.

Почему это было так трудно увидеть

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

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

Неприглядная правда о такого рода работе

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

Это смиряет, причём полезным образом. Математика редко была тем, что останавливало меня чаще всего. Трубы — были.

Исправление: перестать доверять умолчаниям, заставить договориться

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

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

Урок: умолчание — это решение, которое ты не принимал

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

Я потерял добрую часть недели из-за выбора, о котором не знал, что сделал его. Теперь, когда что-то падает так, что в этом нет никакого логического смысла, один из моих первых вопросов больше не только «где мой баг?». Это ещё и «что я предполагаю из того, что на самом деле никогда не решал?».

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