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

Полная версия

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

Язык: Русский
Год издания: 2026
Добавлена:
Настройки чтения
Размер шрифта
Высота строк
Поля
На страницу:
2 из 4

А что же осталось на долю различий? Удивительно мало, и всё оно — на поверхности. C и Java здесь совпадают буква в букву: две строки из разных десятилетий, которые невозможно различить, не зная заранее, где какая. Go переставляет тип: пишет имя переменной прежде её типа и не ставит скобок вокруг условия. Rust меняет ключевое слово — fn вместо int на месте объявления, — помечает тип результата стрелкой -> и позволяет последнему выражению стать возвращаемым значением без слова return. Вот, в сущности, и весь список. Это различия выговора, а не грамматики: где-то иной порядок слов, где-то другая огласовка того же звука. Меняется акцент. Фраза остаётся той же.

Стоит, правда, оговориться сразу, потому что соблазн велик. Сказать «языки одинаковы» было бы и неправдой, и пошлостью. Различия между ними огромны — но живут они не здесь, не в силуэте функции. u64 в Rust обещает то, чего не обещает int; за скобками Java стоит управляемая память, а за скобками C — ручная память и полное доверие к программисту. Вот эти обещания и есть настоящая разница между языками, и она серьёзна. Но она лежит слоем ниже общего синтаксиса, а не в нём самом. Силуэт у всех один; расходятся они в том, что гарантируют под этим силуэтом.

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

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

То, что Ритчи заложил в 1972 году, ведёт себя в точности как наследственный код. Фундамент — функция, тип, блок в скобках, if, return — скопировал себя в Java, оттуда в Go, оттуда в Rust, и при каждом копировании язык-потомок дописывал что-то своё: виртуальную машину, горутины, проверки владения на этапе компиляции. Но несущая последовательность переходила из поколения в поколение нетронутой. Её не пересматривали, потому что незачем; её наследовали, как наследуют форму скелета, — не выбирая и часто не замечая. Это ДНК, которая живёт пятьдесят лет и не собирается умирать.

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

Не «языки похожи» — похожими бывают и чужие друг другу вещи. А «это один язык», проступающий сквозь четыре имени. Хаос наречий, в котором учили выбирать племя, на просвет оказывается единой грамматикой с разными акцентами. И если это так с факториалом — самой простой из задач, — то стоит спросить, далеко ли расходятся языки там, где задачи серьёзнее.

Мы привыкли называть фигурные скобки, if и return синтаксисом одного языка. Но один и тот же факториал проходит сквозь полвека и четыре чужие друг другу культуры почти неизменным — а значит, это не синтаксис одного языка, а общий фундамент всех. Корень назван. Из земли, которую мы разглядывали, поднимается ствол, и видно, что он один.

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

Глава 2. Отцы и дети

Шесть поколений: как каждый язык исправлял ошибки предыдущего

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

Это недовольство — не каприз и не тщеславие. За каждым «я могу лучше» стоит вопрос, на который предыдущий язык ответил так, а не иначе, и ответ этот всегда был выбором одного в ущерб другому. Что важнее — мощь или безопасность? Свобода программиста или гарантии компилятора? Возможность сделать всё — или невозможность сделать опасное? На такие вопросы нельзя ответить раз и навсегда. Можно лишь сместить акцент: усилить одно, ослабив другое, — и тем самым открыть новую трещину там, где прежде был монолит. Создатель языка — не изобретатель синтаксиса, а человек с убеждением о том, что для программиста важнее всего. Синтаксис — лишь форма, в которую это убеждение отлито.

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

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

2.1. C++ (1985) — порядок ценой сложности

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

Это убеждение он принёс не из воздуха. Ещё аспирантом Страуструп работал с Симулой — языком, в котором мир описывался не как поток команд, а как множество объектов, каждый со своим состоянием и кругом доступных действий. Симула давала именно ту организацию мысли, которой не хватало C: программа в ней читалась как модель предметной области, а не как инструкция машине. Но за стройность платили скоростью — Симула была слишком тяжела для задач, где каждая миллисекунда на счету. Страуструп унёс из этого опыта одну неотступную мысль: организацию Симулы нужно совместить со скоростью C, не пожертвовав ни тем, ни другим. Так родилось то, что он сначала назвал «C с классами», — не новый язык, а C, которому добавили способ мыслить вещами. C++ вырос из этой прививки.

За диагнозом стоял вопрос, который C обходил молчанием. Как описать не действие, а вещь — объект, у которого есть свойства и поведение, который знает, что с ним можно делать, а что нельзя? C знал про данные и про функции, но между ними не было связи: структура хранила поля, функции обрабатывали их, и ничто не мешало обработать данные не той функцией. Страуструп захотел связать данные с поведением и оградить внутреннее устройство вещи от внешнего мира. Так в язык вошла структура в большом смысле слова — не struct как набор полей, а способ мыслить программу как сообщество объектов.

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

Но Страуструп сделал ещё кое-что, и в этом «кое-что» спрятана вся драма языка. Он не стал ничего отнимать. C++ остался полностью совместим с C: всё, что было опасного в C, осталось опасным и здесь. Указатели, ручное управление памятью, арифметика адресов, возможность прочитать то, что уже освобождено, или выйти за границу массива и получить не ошибку, а тишину, за которой прячется разрушенная память, — всё это перешло в новый язык нетронутым. К старым опасностям добавились новые, рождённые самой мощью надстройки: множественное наследование, в котором иерархия запутывается сама в себе; перегрузка, в которой один и тот же знак значит в разных местах разное; шаблоны, разворачивающиеся в нечитаемые простыни сообщений об ошибках. Язык умел почти всё — и почти ни от чего не защищал.

У этой незащищённости есть точное имя — undefined behavior. Это область, где язык умывает руки: ты сделал нечто, не разрешённое правилами, и отныне может произойти что угодно — программа упадёт, или выдаст мусор, или будет годами работать правильно, чтобы рухнуть в день, когда это дороже всего. Компилятор не обязан предупреждать; чаще всего он молчит. Ответственность целиком на программисте. Он должен сам, в голове, держать все правила, которые язык не проверяет за него. C++ дал инженеру власть над машиной почти без ограничений — и ровно настолько же оставил его одного перед последствиями.

Была у этого решения и тень иного рода, культурная. Объектная модель C++ оказалась так влиятельна, что на двадцать лет превратилась в догму. Целое поколение училось, что правильно спроектированная программа — это иерархия классов, что почти всё на свете следует моделировать как объект с наследниками, что мышление об архитектуре — это мышление об иерархиях. Идея, рождённая как инструмент против сложности, сама стала источником сложности: системы обрастали слоями абстракций, заведёнными не задачей, а привычкой видеть мир объектно. Лекарство прижилось так прочно, что начало напоминать болезнь.

За всеми этими решениями — конкретное убеждение, и оно объясняет их разом. Страуструп верил, что программисту нужна мощь и что мощь нельзя обрезать ради удобства слабого. Дать инженеру полный контроль над машиной, ничего не пряча и ничего не запрещая, — а он сам разберётся, как этим не навредить. У хирурга скальпель не снабжён предохранителем, и у мастера-краснодеревщика стамеска не затуплена ради безопасности новичка: профессиональный инструмент опасен ровно настолько, насколько остёр, и тупить его — значит делать бесполезным. Страуструп строил инструмент для профессионала. Это вера в зрелость и компетентность того, кто держит инструмент: настоящему мастеру не нужна защита от собственной руки, ему нужно, чтобы инструмент не мешал. C++ построен на доверии к человеку — и в этом доверии его величие и его рана одновременно. Потому что человек устаёт, ошибается и забывает, а undefined behavior — нет.

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

2.2. Java (1995) — безопасность ценой свободы

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

Из этого вырастал вопрос, противоположный вопросу Страуструпа. Не «как дать мастеру полную власть», а «как устроить язык так, чтобы и самый слабый в команде не смог разрушить всё»? Самые дорогие ошибки в C-линии рождались там, где программист сам распоряжался памятью: обращение к уже освобождённому участку, утечки, выход за границу, висячий указатель, который годами притворяется исправным. Ответ Java был радикален в своей простоте — отнять у программиста память вовсе. Сборка мусора взяла на себя освобождение: ты больше не закрываешь то, что открыл, об этом заботится среда. Указатели уступили место ссылкам, которые нельзя двигать арифметикой. Множественное наследование — клубок, в котором путались иерархии C++, — было просто вырезано. Опасный инструмент убирался не затуплением, а изъятием.

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

Из всего этого Java сплела вокруг программиста страховочную сетку. Но в сетке была дыра, и имя ей — null. Убрав опасную память, Java сохранила старую ссылку в никуда — значение, которое означает «здесь ничего нет» и которое система типов не отличала от настоящего объекта. Можно было обратиться к этому ничто как к вещи — вызвать у него метод — и получить не отказ компилятора, а крушение во время работы: NullPointerException, самый знакомый сбой за всю историю языка. Идея null была не нова и не принадлежала Гослингу: человек, введший пустую ссылку в обиход десятилетиями раньше, потом назовёт её своей ошибкой на миллиард долларов. Гослинг унаследовал её, не задумавшись. Сетка ловила всё, кроме одного, — и сквозь это одно проваливались чаще всего.

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

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

За всеми этими решениями — убеждение, и оно зеркально перевёрнуто относительно убеждения Страуструпа. Там, где C++ доверял человеку, Java ему не доверяла. Программисту нельзя доверять память — но можно доверять null. В этой формуле — вся непоследовательность реформы. Недоверие дотянулось до памяти и остановилось перед пустой ссылкой. Почему? Потому что память ощущалась как власть, а null — как безобидное удобство, «просто отсутствие». Опасность, маскирующаяся под удобство, переживает любую реформу: её не замечают именно потому, что она не выглядит опасной. Гослинг отнял у программиста острые инструменты и оставил ему тупой на вид предмет, оказавшийся самым колющим из всех.

И всё же недоверие Java было честным и оправданным. Чтобы понять этот выбор, стоит выйти за пределы программирования. Частная мастерская и общественное здание подчиняются разной логике. В мастерской мастер волен на что угодно: он один отвечает и за результат, и за себя. Но здание, в которое войдут тысячи, строится по своду правил, и свод этот стесняет каждого строителя, запрещая то, что в частной работе сошло бы с рук, — ради того, чтобы оплошность одного не обрушилась на головы всех. Правила делают здания скучнее и крепче. Java выбрала логику свода: она строила не мастерскую для виртуоза, а каркас для большого, разнородного, сменяющегося коллектива, где цена одной ошибки измеряется не репутацией автора, а упавшей системой. За её недоверием стояла не трусость, а ответственность: язык брал на себя миллиарды строк делового кода, который должен работать годами в руках, которые его не писали. Это не провал воображения, а взрослый выбор — пожертвовать свободой немногих сильных ради того, чтобы не падала система, держащаяся на многих обычных. C++ дал мощь и оставил человека одного с последствиями; Java вернула человеку защиту, отняв часть свободы и заставив платить словами. Каждый ответил на свой вопрос, и каждый ответ открыл новую трещину.

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

2.3. C# (2000) — итерация без революции

Андерс Хейлсберг был не теоретиком, а оружейником языков. До C# он сделал Turbo Pascal и Delphi — инструменты, которыми каждый день работали живые программисты, и эта привычка определила его взгляд. Его не занимало, какая идея смелее или чище; его занимало, что именно раздражает человека за клавиатурой и как это раздражение убрать. С такой позиции язык Гослинга выглядел надёжным, но безрадостным: безопасным ценой свободы, защищённым ценой многословия. И вопрос, который поставил Хейлсберг, был скромен до дерзости. Не «какую новую вселенную построить», а «как сделать уже найденное приятным»? Возьмём безопасность Java и её абстрактную машину — и срежем с них обряд. Сделаем Java удобнее.

Ответ складывался не из переворотов, а из множества тщательных сглаживаний. Там, где Java заставляла для каждого поля писать пару обрядовых обёрток для чтения и записи, C# позволил полю самому выступать как свойство, спрятав церемонию внутрь. Это мелочь — но из таких мелочей состоит ежедневный труд. Дальше шли вещи покрупнее. Запросы к данным вошли прямо в язык: коллекцию можно было спрашивать, как спрашивают базу, — данные стали полноправным предметом вопроса, а не грудой, которую перебирают вручную. А затем явился способ писать конкурентный код так, чтобы он читался последовательно, — пара слов, превращавшая запутанную асинхронность в почти линейный текст. Прежде ожидание чего-то медленного — ответа сети, чтения с диска — рассыпало логику на клочки: ты передавал продолжение как отдельный кусок, который выполнится потом, и программа разваливалась на гнёзда обратных вызовов, где порядок чтения уже не совпадал с порядком исполнения. Два слова собрали эти клочки обратно: код снова читался сверху вниз, как рассказ, а машина сама распутывала, что за чем дождаться. C# дал этот способ за годы до того, как он стал всеобщим. Модель, по которой потом будет писать полмира, впервые заработала здесь.

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

И всё же каждое из этих улучшений жило за стеной. C# был привязан к одной экосистеме — к Windows, к платформе. NET, к коммерческому миру одной компании. Долгие годы он не работал там, где жил открытый мир, где складывалась культура свободного, кроссплатформенного кода. И потому его находки, нередко первые, доставались огороженному саду, а не общему полю. Способ писать асинхронность по-человечески существовал здесь раньше, чем где бы то ни было, — но профессия в массе своей выучила этот приём позже и у других языков, перенявших его следом. Лучший ответ был дан в комнате, в которую большинство так и не вошло.

За всем этим стояло убеждение, и оно было тише, чем у предшественников, оттого не менее твёрдым. Не нужна революция — нужна хорошая итерация. Хейлсберг не пытался опрокинуть C-линию и переписать парадигму набело; он верил, что движение вперёд — это терпеливое снятие трения, версия за версией, год за годом. Эту же веру он позже подтвердит ещё раз, взявшись за JavaScript: не заменить его новым языком, а тихо надстроить сверху слой типов, сделав привычное безопаснее, не отняв привычного. Один и тот же инстинкт, приложенный дважды, — это уже не случай, а мировоззрение. И история доказала его правоту: именно так, итерацией, а не переворотом, и движется вся линия. Но у этой правоты оказалась жестокая сноска. Он был прав — а мир, занятый другим, этого не заметил. Тихая правота.

В этом — особая, почти грустная фигура среди создателей. Не пророк, отвергнутый при жизни, и не бунтарь, опрокинувший прежний порядок, а мастер, который раньше многих нашёл верное решение и получил за него меньше всех признания — потому что говорил на наречии, слышном лишь за стеной. У этой судьбы есть точный двойник за пределами программирования. Монах, открывший законы наследования на грядке гороха, опубликовал их в безвестном издании — и они пролежали незамеченными десятилетия, пока другие, заново придя к тому же, не нашли их готовыми. Открытие было верным с первого дня; не хватало лишь того, чтобы оно прозвучало там, где слушают. Так и здесь: правоту Хейлсберга подтвердят не аплодисментами, а тем, что те же приёмы один за другим всплывут в чужих языках как собственные открытия. Идея, рождённая в огороженном саду, прорастёт повсюду — но уже без имени садовника.

Среди шести наследников C# — самый чистый случай движения вперёд без разрушения. Он почти ничего не отнял у Java: взял её устройство как есть и принялся прибавлять — удобство за удобством, версию за версией. Других наследников тянуло то отнять опасное, то снести и построить заново; C# не сносил и почти не отнимал. Вся его драма уместилась не в том, что он сделал, а в том, где он это сделал: единственным крупным компромиссом стала привязка к домену одной компании, и именно она, а не нехватка идей, держала его в тени. Чистая прибавка плюс цена места рождения — вот вся его арифметика.

На страницу:
2 из 4