Базовый CS с нуля
Зачем существует абстракция
Ты встретил три формы абстракции: имя, скрывающее деталь, объект, связывающий данные с операциями, модуль, скрывающий код за границей. Каждая что-то скрывает. Но сокрытие — это средство, а не цель. Этот урок задаёт прямой вопрос: для чего всё это сокрытие на самом деле нужно?
Ответ — одно слово, сложность, — и ты проживал этот ответ весь трек. Оглянись. Ты начал с вентилей, которые либо включены, либо выключены. Из вентилей вышел машинный код. Из машинного кода вышел язык программирования. Из языка вышли значения, переменные, поток выполнения, функции. Каждая ступень позволяла перестать думать о ступени под ней.
Эта лестница — не учебный приём. Это то, как строятся настоящие системы, и это то, для чего абстракция нужна. Этот урок называет паттерн, показывает, почему он работает, а затем показывает цену, которую за него платишь, — потому что абстракция не бесплатна.
После этого урока ты сможешь объяснить, как абстракция управляет сложностью, складывая слои, описать, что значит рассуждать на одном уровне без уровня под ним, распознать весь трек как один такой стек слоёв и объяснить, что такое протекающая абстракция и почему все нетривиальные абстракции протекают.
Проблема, которую решает абстракция: сложность. Сложность здесь означает само количество отдельных вещей, которые тебе пришлось бы держать в уме сразу, чтобы понять систему.
Современная программа, прослеженная до самого низа, — это миллиарды переключений вентилей в секунду. Никакой человеческий ум не может держать миллиарды чего бы то ни было. Если бы понимание одной строки кода требовало отслеживать каждый вентиль, который она в итоге переключает, программирование было бы физически невозможно — не трудно, а невозможно, так же как невозможно запомнить каждый атом в мосте.
Абстракция — это ответ на эту невозможность. Она не заставляет миллиарды вентилей исчезнуть; они по-прежнему переключаются. Она делает их тем, о чём не нужно думать. Цель каждой абстракции в этом разделе и каждой абстракции, которой ты когда-либо воспользуешься, — эта одна задача: сжать то, что нужно держать в уме, до размера, который человек действительно способен удержать.
Механизм: стек слоёв. Абстракция побеждает сложность, складывая слои. Слой — это один уровень абстракции, построенный поверх слоя под ним: он использует интерфейс нижнего слоя и предлагает новый, более высокий интерфейс собственный.
Правило, которое заставляет стек работать: каждый слой позволяет рассуждать, используя только интерфейс этого слоя, не держа слой под ним в голове. Когда ты пишешь вызов функции, ты рассуждаешь на слое языка — значения, имена, вызовы. Ты не рассуждаешь одновременно о машинном коде и уж точно не рассуждаешь о вентилях. Слои под ним по-прежнему выполняются. Их просто нет в твоей голове.
Это сделка из урока 01, повторённая вверх. Каждый слой скрывает тот, что под ним. Сокрытие складывается: с твоего места наверху ты видишь один интерфейс и думаешь об одном интерфейсе, хотя под каждой строкой работает башня скрытого механизма.
Весь трек был одним стеком слоёв. Ты не просто узнал о слоях — ты взобрался по их стеку, раздел за разделом.
- Вентили (раздел 01) — переключатели вкл/выкл. Нижний слой.
- Машинный код (разделы 03–04) — инструкции, построенные на вентилях.
- Язык программирования (раздел 04) — читаемый код, построенный на машинном коде.
- Конструкции (разделы 05–10) — значения, переменные, поток выполнения, функции, объекты, модули — всё построено на языке.
Каждый раздел позволял перестать выводить заново тот, что под ним. К разделу 08 ты вызывал функцию, не думая о машинном коде. К этому разделу ты связываешь объекты и модули, не думая о переменных-как-ячейках. Это стек слоёв, делающий свою работу: каждая ступень, на которую ты взобрался, стала тем, что можно использовать и перестать выводить заново. Сам трек был наглядной демонстрацией того, зачем существует абстракция.
Компромисс: абстракции протекают. У абстракции есть цена, и притворяться, что её нет, — само по себе ошибка. У цены есть имя: протекающая абстракция.
Абстракция протекает, когда не полностью скрывает слой под ней — когда деталь снизу проступает и тебя вынуждают думать об уровне, который тебе обещали игнорировать. Протечки случаются чаще всего, когда нижний слой падает или работает медленно: пока всё работает, скрытый слой остаётся скрытым, но в момент, когда он ломается или тормозит, он выходит на поверхность.
Джоэл Спольски сформулировал это как Закон протекающих абстракций: все нетривиальные абстракции в той или иной степени протекают. Нельзя построить идеальную, никогда не протекающую абстракцию над настоящей машиной. Протечка не означает, что абстракция плоха — она означает, что абстракция реальна, что она стоит поверх настоящего механизма, который по-прежнему может упасть. Практическое следствие: абстракция экономит тебе время работы, но не время изучения. Тебе всё равно приходится понимать слой под ней на тот день, когда он протечёт.
Почему это работает
Почему слой нельзя просто запечатать идеально? Потому что слой под ним по-прежнему делает настоящую работу, а настоящая работа может пойти не так способами, которые интерфейс никогда не описывал. Интерфейс функции обещает «два числа на вход, сумма на выход» — он ничего не говорит о том, сколько времени это займёт или что произойдёт, если машине посреди вызова не хватит памяти. Время, ограничения памяти и аппаратные сбои — это реальные свойства нижнего слоя, которые никакой более высокий интерфейс не может полностью поглотить. Интерфейс описывает результат; он не может убрать описанием физику. Этот разрыв — то, откуда берётся каждая протечка.
Замечаем протечку: стек вызовов проступает.
Вспомни стек вызовов из урока 02 раздела 08. Абстракция вызова функции даёт обещание:
вызови функцию, она выполнится, она вернётся. Ты рассуждаешь на слое языка — f()
выполняется и возвращается. Кадры стека, адреса возврата, конечная область стека — всё это
скрытая реализация. Тебе сказали, что её можно игнорировать.
Пока всё работает, ты можешь. Ты пишешь f() тысячи раз и ни разу не думаешь о стеке.
Абстракция держится. Слой под ней остаётся скрытым.
Теперь заставь нижний слой упасть. Напиши функцию, которая вызывает саму себя и никогда не останавливается. Кадр за кадром кладётся; конечная область стека заполняется; программа падает с переполнением стека. Внезапно сообщение об ошибке говорит о стеке — детали слоя под абстракцией вызова функции. Реализация проступила.
Прочитай ситуацию через этот урок. Абстракция вызова функции протекла. Пока нижний слой работал, он оставался скрытым; в момент, когда он достиг своего предела, он вышел на поверхность, и тебя вынудили снова рассуждать о кадрах стека — ровно об уровне, который абстракция обещала скрыть. Это Закон протекающих абстракций в одном конкретном событии: абстракция была по-настоящему полезна для тысяч обычных вызовов, и она протекла в тот миг, когда слой под ней сломался. И то, и другое верно. Вот почему ты изучил стек вызовов, хотя функция «скрывает» его — чтобы, когда он протечёт, ты всё равно мог рассуждать.
Частая ошибка
Распространённая ошибка — заключить, что, раз абстракции протекают, ими не стоит пользоваться, — или наоборот, что достаточно хорошая абстракция никогда не протечёт. И то, и другое неверно. Абстракция не факультативна: без неё никакую программу длиннее нескольких строк вообще нельзя было бы написать. И никакая абстракция над настоящей машиной не защищена от протечек. Зрелый взгляд держит оба факта сразу: пользуйся абстракцией ради рассуждения, которое она даёт, и понимай слой под ней на тот день, когда он протечёт. Именно поэтому этот трек научил тебя слоям под теми, в которых ты в основном будешь работать.
Главная задача абстракции — управлять одной вещью: количеством отдельных вещей, которые нужно держать в уме, чтобы понять систему. Введи 1, если эта вещь — сложность, 0, если это скорость.
Стек слоёв трека, снизу вверх: вентили, машинный код, язык, конструкции. Сколько это слоёв?
Абстракция протекает, когда деталь слоя под ней проступает. Абстракция вызова функции прекрасно работает для 10000 обычных вызовов, затем неуправляемая рекурсия вызывает ошибку переполнения стека. Протекла ли абстракция? Введи 1 за «да», 0 за «нет».
Закон протекающих абстракций гласит, что все нетривиальные абстракции в той или иной степени протекают. Из 5 нетривиальных абстракций сколько гарантированно полностью защищены от протечек?
Абстракция экономит время работы, но не время изучения. Если слой под ней всё равно нужно изучить на тот день, когда он протечёт, сколько слоёв можно полностью игнорировать навсегда — вообще никогда не понимая их?
Зачем существует абстракция и какой компромисс с ней приходит?
Абстракция существует, чтобы управлять сложностью — количеством отдельных вещей, которые нужно держать в уме, чтобы понять систему. Она делает это, складывая слои: каждый слой построен на интерфейсе того, что под ним, и предлагает новый интерфейс собственный, позволяя рассуждать, используя только этот слой, не держа нижние слои в голове. Весь трек был одним таким стеком — вентили, затем машинный код, затем язык, затем конструкции — и каждый раздел позволял перестать выводить заново тот, что под ним. Компромисс — это протекающая абстракция: слой, который не полностью скрывает тот, что под ним, поэтому деталь проступает, чаще всего когда нижний слой падает или работает медленно. Закон протекающих абстракций гласит, что все нетривиальные абстракции в той или иной степени протекают, и поэтому абстракция экономит тебе время работы, но не время изучения — и поэтому этот трек научил тебя каждому слою под теми, в которых ты в основном будешь работать.