Программирование как мышление. Как языки полвека искали одну форму

- -
- 100%
- +
C# улучшал прибавлением — со вкусом, с тактом, но всегда добавляя. И каждое поколение до сих пор двигалось так же: структура, безопасность, удобство — куча росла. Настал момент, когда кто-то задал противоположный вопрос. Что, если улучшение — это не добавить правильное, а убрать неправильное? Что, если язык становится лучше не оттого, что в нём прибавилось, а оттого, что из него вычли? Не поправка внутри общего направления, а бунт против самого направления.
2.4. Go (2009) — бунт простоты
У этого языка особое место в семейной хронике, и дело не только в его устройстве. Среди его создателей — Роб Пайк, Роберт Гризмер и Кен Томпсон, а Томпсон был одним из тех, кто стоял у самого корня: он строил Unix и язык, из которого вырос C. Получается почти буквальная сцена: отец, заложивший фундамент, спустя десятилетия возвращается и смотрит на то, во что разрослось его дело. И то, что он увидел вместе с соавторами в стенах Google, было не нехваткой, а избытком. Огромные сборки, тянущиеся минутами; кодовая база, заросшая слоями абстракций; язык, в котором, чтобы понять чужой файл, нужно сперва распутать иерархию. Диагноз был противоположен всем прежним. Предшественники чинили линию, добавляя ей недостающее; эти трое сказали иное. Всё стало слишком сложным.
И ответ был так же противоположен. Каждое поколение до сих пор двигалось вперёд, прибавляя; Go двинулся вперёд, отнимая. Из языка вырезали наследование — те самые запутанные иерархии, и на их место поставили простое сложение частей: вещь собирается из других вещей, а не выводится из предков. Вырезали исключения — невидимый поток управления, который прыгает туда, где его не ждёшь, — и вернули ошибку на свет, сделав её обычным возвращаемым значением. Убрали тяжёлого посредника между программой и машиной: никакой виртуальной машины, программа собирается в один самодостаточный двоичный файл, который просто запускается где угодно, и собирается за секунды, а не за минуты. Развёртывание, прежде требовавшее установить на машину целую среду нужной версии, свелось к тому, чтобы скопировать туда один файл, — и всё. Программа перестала тащить за собой шлейф зависимостей; она снова стала вещью, а не обрядом установки. А взамен дали лёгкость, которой ни у кого не было, — горутины, способ запускать тысячи параллельных задач, не задумываясь о цене каждой. Прежде поток был тяжёл: их заводили поштучно и считали, как редкий ресурс. Горутина оказалась так дёшева, что считать перестали вовсе — конкурентность из дефицита, который берегут, превратилась в воздух, которым дышат. И связывались эти задачи не общей памятью, за которую дерутся, а передачей сообщений: не «поделим один кусок и будем сторожить его друг от друга», а «передадим из рук в руки». Язык умещался в голове целиком. Новичок открывал любой файл и понимал его без проводника. Простота была здесь не следствием бедности, а сознательно выбранным решением.
В этом есть жест, знакомый далеко за пределами программирования. Скульптор перед глыбой мрамора не прибавляет — он убирает всё, что не есть фигура, пока она не проступит сама. Совершенство достигается не тем, что уже нечего добавить, а тем, что нечего отнять. Вся прежняя линия работала как живописец, накладывающий слой за слоем; Go впервые взял в руки резец и стал работать как скульптор — улучшая отсечением. Это другой способ мыслить инструмент: не «чего ему не хватает», а «что в нём лишнее».
Но у вычитания есть дно, и Go в него упёрся. Старую дыру он не залатал: nil перешёл в него нетронутым, всё та же пустая ссылка, всё та же тишина перед сбоем. И в этом — тихая улика родства. Язык, отвергнувший наследование, исключения и тяжёлую среду исполнения, язык, объявивший войну всему лишнему, что накопила линия, — этот самый язык сохранил её древнейшую ошибку, ту, что тянется от первой пустой ссылки. Можно восстать против отцов, отбросить их манеру, их сложность, их догмы — и всё равно нести в себе их старейший просчёт, не замечая его, потому что он давно перестал выглядеть как ошибка. Иное наследство глубже любого бунта. Честность с ошибками он довёл до обряда — if err!= nil, повторяющееся в конце каждого шага, как удары пульса: правдиво, но утомительно, страница за страницей одно и то же. А главное — он больше десяти лет отказывался от обобщённого кода. Нельзя было написать одну функцию для многих типов, не копируя её или не теряя проверку; язык, превыше всего ценивший простоту, заставлял повторяться. Не было и способа сказать «это одна из таких-то форм» — ни ясного выбора по структуре, ни суммы вариантов. Минимализм, бывший достоинством в центре, к краям оборачивался нехваткой.
А потом, в 2022 году, в версии 1.18, Go добавил то самое обобщение, от которого открещивался все эти годы. И само его долгое сопротивление было результатом эксперимента, а не упрямством. Вычитание работает прекрасно — ровно до того дня, когда отнятое начинает стоить дороже сбережённой простоты. Эту границу Go не вывел из теории, а нащупал сам — и, дойдя до неё, сам же признал.
За всем этим — убеждение, перевёрнутое относительно всех предыдущих. Лучше убрать лишнее, чем добавить правильное. Там, где каждый предшественник верил, что прогресс — это верная прибавка, Go поверил, что прогресс — это верное изъятие. И это был настоящий эксперимент, а не вопрос вкуса: можно ли сделать язык лучше, отнимая? Ответ, который дал Go, точен и обоюдоостр. Да — но лишь до предела. Простота покупает ясность, пока не начинает стоить возможностей; за этой чертой дно отталкивает обратно.
В этом ответе есть та же фигура, что и во всех прежних, только вывернутая наизнанку. Страуструп добавил мощь и получил сложность. Гослинг добавил защиту и получил многословие. Go убрал сложность — и получил нехватку выразительности. Каждый закрывал чужую слабость, обнажая свою; направление менялось, закон оставался. Сильная сторона решения и его слабость растут из одного корня, и нельзя усилить одно, не оголив другое.
Go восстал, вырывая лишнее. Он доказал силу меньшего и нашёл его границу. Следующий шаг будет сделан с противоположным чувством — не бунтом против наследства, а починкой его: взять несовершенную вещь и исправить её, не ломая, сохранив работающее и залатав сломанное.
2.5. Kotlin (2011) и Dart (2011) — два пути реформы
К 2011 году Java была рабочей лошадью уже шестнадцать лет, и её слабости давно перестали быть загадкой. Дыра, через которую проваливался null; многословие, в котором тонула мысль; обряд, наросший вокруг простых действий, — всё это было не тайной, а хорошо составленной картой ран. И в один и тот же год две команды взялись лечить один и тот же язык. Удивительно не то, что они взялись, а то, что разошлись на первом же шаге — на вопросе, который встаёт перед всяким, кто хочет исправить старое жильё. Чинить дом, в котором живёшь, или построить новый рядом? Диагноз был общий. Стратегии — противоположные.
Kotlin выбрал первый путь — эволюцию изнутри. За ним стояла JetBrains, компания, делавшая инструменты для программистов и потому знавшая ежедневную боль Java не по рассказам, а изнутри, как знает дом жилец, проживший в нём годы. Её ставка была осторожной до дерзости: не покидать виртуальную машину Java, не рвать совместимость — исправлять язык, не выходя из него. Kotlin работал на той же машине, вызывал Java и вызывался из неё; проект можно было переводить файл за файлом, живя сразу в двух языках, не выселяясь ни на день. Это и есть починка дома, в котором живёшь: один файл ещё на старом наречии, соседний уже на новом, и компилятор не видит между ними границы — стены подновляют по комнате, не выгоняя жильцов на улицу. И внутри этого тесного условия он залечил знаменитые раны. null стал частью системы типов: тип, в котором может не оказаться значения, теперь пишется иначе, чем тип, в котором значение есть всегда, и компилятор не позволит их перепутать. Вопрос «как выразить отсутствие?», оставленный Java без верного ответа, получил ответ внятный и обязательный. Обряд исчез: то, что прежде требовало десятков строк обёрток — описать данные, а потом вручную научить их сравниваться, печататься, отдавать и принимать поля, — свелось к одной строке; данные снова стали просто данными, без церемонии вокруг них. Корутины дали дешёвую конкурентность. «Java, каким он должен был быть» — не новый мир, а тот же самый мир, починенный. И мир согласился: спустя несколько лет крупнейшая платформа, на которой жила Java, объявила Kotlin предпочтительным языком — редкий случай, когда «каким должен был быть» признаётся официально, поверх оригинала.
Dart выбрал второй путь — перестройку с нуля. Ставка Google была обратной: не латать, а строить заново. Dart был новым языком с собственным окружением, не привязанным к платформе Java, и с самого начала нацеленным на определённую область — на построение интерфейсов. Он тоже залатал дыру null — гарантированную сквозную защиту от пустой ссылки он довёл до конца в 2021 году. Он тоже срезал многословие, дал асинхронность по-человечески, дал способ подмешивать поведение в разные сущности, минуя жёсткие иерархии. Но он требовал переезда: новый дом, новый фундамент, а старый ты оставляешь позади. И у этого дома была трудная судьба. Поначалу Dart метил совсем в другую цель — стать заменой языку браузера, встать на место JavaScript прямо внутри Chrome; этот замысел не сбылся, встроенной машины в браузере так и не появилось, и язык остался стоять с фундаментом, но без жильцов. Долгие годы Dart дрейфовал, язык в поисках причины существовать, — пока Flutter не дал ему эту причину, способ собирать интерфейсы сразу для многих платформ; и тогда новый дом наконец обрёл жильцов. Язык, которому дважды искали назначение, нашёл его не в том, для чего строился, — обычная участь того, кто заложил дом прежде, чем понял, кто в нём будет жить. Компромисс домена — интерфейсы — вписан в самую его суть.
Сними подробности — и под ними окажется одна развилка. Оба языка — дети Java, оба чинят одни и те же две раны, null и многословие, и оба исповедуют одно и то же. Ни один не восстал, как Go, вырывая лишнее; ни один не заперся, как C#, за стеной экосистемы. Они расходятся только в стратегии. Чинить старый дом, оставаясь внутри, пока подновляешь стены, — или построить новый рядом и позвать всех переехать? Правильного ответа здесь нет — есть темперамент. Kotlin доверяет преемственности: лучшее в старом доме стоит сохранить, переезд дорог и рискован. Dart доверяет чистому началу: на старом фундаменте всех ошибок не исправишь, иногда честнее заложить новый. Две дороги от одной двери, и обе ведут прочь от Java — но в разные стороны.
И у каждой дороги своя цена — тот же закон, что правил всеми предыдущими. Кто чинит старый дом, наследует не только его стены, но и его фундамент: Kotlin остался на машине Java и вместе с её охватом принял её границы — он не может уйти дальше, чем позволяет почва, на которой стоит. Кто строит новый, остаётся на пустом участке один: Dart свободен от старого фундамента, но и отрезан от всего, что на нём выросло, — от библиотек, привычек, накопленного мира Java, ради которого многие и не трогаются с места. Преемственность платит несвободой. Чистое начало платит одиночеством. Усилить одно, не оголив другого, не вышло и здесь.
Примечательно, к чему оба тянутся прежде всего. Не к новым возможностям, не к собственным изобретениям — к старой дыре. Флаг, который каждый поднимает первым, — безопасность от null. Рана, которую Java оставила, вопрос, на который она не сумела ответить, становится сердцем обеих реформ. Один и тот же изъян независимо тянет к себе одну и ту же починку, словно к больному месту само собой возвращается лекарство. За этим стоит общее убеждение, тихое и твёрдое: можно исправить прошлое, не разрушая его. Там, где Go сказал «вырвать», Kotlin и Dart сказали «залечить». Не революция, а реформа; не разрыв, а продолжение. Уважение к работающему наследству, которое не сжигают за то, что в нём есть трещины, а чинят, потому что в нём слишком много живого.
Но обе реформы лечили по частям. Каждая починка была местной: null здесь, обряд там, конкурентность в углу. Прицельно, терпеливо, поодиночке — одна рана за раз, оставляя остальное на потом или на чужие руки. И ни один из них не задал вопроса, который висел над всей линией с самого начала. Что, если не исправлять изъяны по очереди, а попытаться закрыть все ошибки разом — в одном языке, с самого основания, не уступив ни в одном компромиссе?
2.6. Rust (2015) — попытка закрыть всё разом
У всей линии до сих пор была одна молчаливая аксиома, которую никто не оспаривал, потому что она казалась законом природы: за всё надо платить. C++ взял мощь и заплатил опасностью. Java взяла безопасность и заплатила свободой. Go взял простоту и заплатил выразительностью. Каждый соглашался на сделку, спорил лишь о её условиях — что отдать, что получить, — но самого торга не отменял никто. Нельзя иметь всё сразу; можно только выбрать, чем пожертвовать.
Грэйдон Хоар, начавший Rust и нашедший ему дом в Mozilla, поставил под сомнение саму аксиому. Его замысел был ересью против закона торга: производительность C++ и безопасность — вместе, без компромиссов. Не выбрать между скоростью и защитой, как выбирали все, а отказаться выбирать. Взять то, за что C++ платил undefined behavior, и то, за что Java платила свободой, — и получить оба, не заплатив ни тем, ни другим. Если каждый предшественник закрывал одну рану, обнажая другую, то Rust замахнулся закрыть их все сразу, не открыв ни одной новой. Это была вершина амбиции, к которой спираль шла полвека.
И замахнуться так он мог лишь потому, что пришёл последним. Он стоял на всех прежних попытках — и, что важнее, на всех задокументированных неудачах. Ему не нужно было гадать, какие раны линии настоящие: полвека уже составили их список. Где Страуструп ещё нащупывал, что вообще болит, Rust получил готовую карту всех болезней разом — и потому смог лечить не наугад, а по полному перечню. Привилегия опоздавшего: вся история ошибок лежала перед ним открытой.
Он пошёл по списку ран, накопленных линией, закрывая каждую как ответ на давний вопрос. Старейшая рана — ручная память и undefined behavior: как дать программисту полный контроль над памятью и при этом не дать ему разрушить её? C++ доверил человеку и проиграл; Java отняла память и заплатила посредником-сборщиком. Rust нашёл третий путь — ownership. Компилятор сам отслеживает, кто владеет каждым значением и когда оно освобождается, и доказывает ещё до запуска, что к освобождённому уже не обратятся. Правило почти аскетично в своей простоте: у каждого значения ровно один владелец; кончился владелец — освободилось значение; одолжить его можно, но компилятор считает все одолжения и не даёт двум рукам писать в него разом. Из трёх несложных правил безопасность памяти выпадает как теорема, а не как надежда. Безопасность памяти без сборщика, без среды-надзирателя — ровно то, что C++ и Java решили каждый лишь наполовину.
Дальше — дыра на миллиард долларов. Как выразить отсутствие так, чтобы его нельзя было проигнорировать? Option
Стоит сказать честно: почти ничего из этого Rust не изобрёл. Отдельный тип для отсутствия, ветвление по структуре, общее поведение без иерархий — всё это десятилетиями жило в других языках, в стороне от большой дороги, верное, но бездомное. Заслуга Rust была не в изобретении ответов, а в том, что он собрал их в одно тело, которое индустрия наконец смогла принять. Он не открыл — он составил. Правильные идеи, ждавшие языка, способного их вынести, дождались его.
Но закон торга не отменить — можно лишь перенести цену в другое место, и Rust перенёс её на программиста. Компилятор, гарантирующий так много, требует доказать, что код достоин гарантии, — а доказательство даётся тяжело. Borrow checker, проверяющий владение, стал знаменитым противником: он отвергает код, который на вид безупречен, и заставляет перестраивать само мышление о том, кто чем владеет и как долго. Новичок пишет то, что собралось бы в любом другом языке, и смотрит, как это раз за разом отклоняют по причинам, поначалу неотличимым от придирок, — пока однажды модель не щёлкнет, и не окажется, что все эти отказы исподволь учили способу думать, который теперь уже не разучиться. Ownership — это реальный барьер, а не пустяк, о который спотыкаются новички; о него спотыкаются и опытные, и многие отступают. За «без компромиссов» заплачено не потерей возможностей, а крутизной подъёма. Сделка не исчезла — она переехала из программы в голову того, кто её пишет.
За всем этим — убеждение, и оно зеркально перевёрнуто относительно самого первого. Страуструп доверил человеку всё. Java не доверила ему память, но доверила null. Rust не доверяет человеку вовсе — и выносит знание из ненадёжной головы в неусыпный компилятор. Гарантии больше не держатся на дисциплине; они держатся на доказательстве. Компилятор должен знать больше программиста — это и есть символ веры Rust. Мастер, который не даст тебе порезаться, потому что проверяет каждое движение лезвия прежде, чем оно тронется.
И здесь спираль доходит до вершины. Страуструп исправил одно. Каждый следующий исправлял одно или два. Rust попытался исправить всё разом — самый амбициозный виток за полвека, язык, построенный, чтобы закрыть все раны линии в одном теле. Шире, чем сюда, амбиция уже не дотянется. Спираль раскрутилась до предела. Шесть поколений, каждое поправляет предыдущее, каждая поправка укладывается в следующую — и паттерн проступил так отчётливо, что его пора назвать. А названный, он сам задаёт вопрос. Если все исправляют одно и то же и тянутся к одним и тем же ответам, то спираль не просто раскручивается всё шире. Она куда-то идёт.
2.7. Формула: паттерн, который повторяется неизбежно
Шесть разборов, шесть поколений, шесть убеждений — и за всеми ними один и тот же ход, повторённый с такой настойчивостью, что его пора назвать вслух. Каждый язык брал предыдущий, убирал то, что в нём болело, добавлял то, чего не хватало, и уступал кое-что своей области применения. Из этого складывается формула, по которой устроена вся линия:
Новый = Предыдущий — Ошибки + Решения ± Компромиссы домена.
Проверим её на пройденном. C++ взял C, почти ничего не убрал, добавил структуру — и заплатил сложностью. Java взяла C++, убрала ручную память и указатели, добавила безопасность и абстрактную машину, уступила деловой среде многословием. C# взял Java, не стал почти ничего убирать, добавил удобства — и заплатил привязкой к домену одной компании. Go взял всю отяжелевшую линию, убрал из неё наследование и исключения, добавил простоту и горутины, уступил масштабу. Kotlin и Dart взяли Java, убрали null и обряд, добавили безопасность и краткость, уступили — один платформе, другой области интерфейсов. Rust взял весь список ран сразу, убрал их все, добавил гарантии и заплатил крутизной подъёма. Формула выдержала все шесть подстановок без исключения.
У формулы есть и то, чего три её знака не показывают: что каждый язык оставил нетронутым и во что при этом верил. C++ сохранил указатели и undefined behavior, потому что доверял программисту мощь. Java оставила null и многословие, рассудив, что памяти доверять нельзя, а null — можно. C# не тронул стену чужой экосистемы, считая себя не революцией, а лишь следующей итерацией. Go удержал nil и обряд проверки ошибок, исходя из веры, что лучше убрать, чем добавить. Kotlin и Dart примирились с границами выбранной стратегии, убеждённые, что прошлое можно чинить. Rust сохранил одну лишь крутизну подъёма — цену за символ веры, что компилятор знает больше человека. Разные слова, разные годы, разные компании — а ход один.
И если смотреть не на отдельный язык, а на всю линию, проступает одно непрерывное движение: от C к Rust тянется не россыпь несвязанных языков, а единый след, где каждый шаг отталкивается от предыдущего и уходит в ту же сторону. Это не шесть историй. Это одна история, рассказанная шесть раз.
Стоит вглядеться в знаки формулы. Минус и плюс — общий хребет: все убирают ошибки, все добавляют решения, и в этом они неотличимы. А вот последний член, ± компромиссы домена, — место, где живёт лицо каждого. Именно он объясняет, почему C# пахнет Microsoft, Dart — интерфейсами, Go — масштабом Google, почему у каждого языка свои идиомы, своя культура, своё сообщество, свой характер. Хребет один, но угол наклона у каждого свой, и из этого угла вырастает всё, что мы принимаем за непохожесть. Здесь важно не оступиться в плоское «языки одинаковые» — они не одинаковы. Различие реально, но оно живёт в последнем члене, а не в первых двух. Общее — движение; уникальное — компромисс, которым каждый платит своей задаче. Под культурным слоем — один ход; в культурном слое — шесть разных лиц.
И раз есть формула, по ней можно считать вперёд. Возьмём нынешний край — Rust, закрывший всё ценой брутального подъёма, — и подставим в ту же формулу. Что уберёт следующий? Очевиднее всего — ту самую крутизну, борьбу с компилятором, цену входа. Что добавит? Те же гарантии, но достижимые без многолетней ломки мышления. Чем уступит? Своей области — как уступали все. Имени следующего языка мы не знаем, но его форму уже можем набросать: безопасность Rust без боли Rust, плюс компромисс под свою задачу. Паттерн стал не наблюдением, а законом, с которым можно работать как с расчётом, — и сама эта предсказуемость должна насторожить. Если будущее линии вычисляется по одной строке, значит, у линии есть не только прошлое, но и направление.
У этой формулы есть и другое свойство, от которого становится не по себе: в ней нет конечного звена. Всякий «Новый», едва родившись, мгновенно становится «Предыдущим» для следующего уравнения. Rust — не конец линии, а её сегодняшний вход. А значит, ни один язык не последний: каждый — лишь лучший на сегодня ответ, несущий в себе ещё не найденную собственную ошибку и ждущий того, кто посмотрит на него и скажет знакомое «я могу лучше». Спираль не останавливалась полвека именно потому, что у формулы нет неподвижной точки. Или всё же есть — и мы её просто пока не назвали?
А направление — это уже не про отдельные языки. Создатели не сговаривались. Они работали в разные десятилетия, в разных странах, для разных задач, и каждый считал, что строит своё. Но все убирали одни и те же ошибки и тянулись к одним и тем же решениям — к безопасности от пустоты, к ветвлению по форме, к гарантиям, вынесенным в компилятор. Идущие порознь, они пришли в одну сторону. Значит, они не разбредались — они сходились. И тогда спираль, которая виток за витком казалась раскручивающейся всё шире, оборачивается чем-то иным: не расширением без конца, а движением к точке.
Виток за витком спираль раскручивалась — каждый создатель смелее предыдущего, каждая амбиция шире, каждое «я могу лучше» громче прошлого, — и казалось, ей нет ни конца, ни центра, только вечное расширение наружу. Но когда последний виток дошёл до предела и шесть путей легли рядом, стало видно то, чего не видно было изнутри движения: у спирали есть центр. Все эти годы она раскручивалась не наружу, а внутрь — к одной точке, которую никто из шестерых не назвал, потому что каждый видел лишь свой виток.



