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

- -
- 100%
- +

Дизайнер обложки Created with Grok
© Сергей Кирницкий, 2026
© Created with Grok, дизайн обложки, 2026
ISBN 978-5-0070-5762-2
Создано в интеллектуальной издательской системе Ridero
Часть I. Одно дерево
Одна линия с фигурными скобками
Разработчик, проживший в профессии несколько лет, носит в голове небольшой музей языков. C — суровый и честный. Java — обстоятельная, тяжёлая на подъём. Python — приветливый. Go — нарочито скромный. Rust — требовательный и строгий. Каждый кажется отдельным миром со своими нравами, своей мифологией, своим племенем. И первое, чему учат новичка, — выбрать, к какому племени примкнуть.
Но стоит положить рядом четыре коротких фрагмента, написанных в разные десятилетия на разных языках, как сквозь различие проступает странное единообразие. Те же фигурные скобки очерчивают тело функции. То же слово return отдаёт результат наружу. То же if разводит исполнение на две ветви. Меняются имена, меняется огласовка — грамматика остаётся прежней. Так в речи незнакомца вдруг узнаёшь падежи родного языка, и разноголосица наречий оказывается делом поверхности.
Узнанное родство — это улика. Улика общего предка, от которого все эти языки унаследовали скелет. И если предок один, то их создатели — не основатели независимых религий, а наследники: каждый получил готовую грамматику и распорядился ею по-своему. За каждым их решением стоит не техническая деталь, а убеждение — что важнее: свобода или безопасность, контроль или абстракция, мощь или простота. Создатель языка — мыслитель со своей верой и своим слепым пятном, а язык — отпечаток этой веры в синтаксисе.
Перед нами генеалогическое древо. Пока мы стоим под ним и, задрав голову, разглядываем ветви — как они расходятся, спорят друг с другом, тянутся в разные стороны. Снизу кажется, что ветвей много и растут они врозь. Чтобы увидеть ствол, придётся спуститься к корню.
Глава 1. Точка ноль
ALGOL, Ритчи и рождение синтаксиса (1958—1972)
Спросите любого, откуда взялись фигурные скобки, if, for, struct, — и услышите: «Это синтаксис C». Ответ звучит так естественно, что в нём не замечают арифметической неувязки. C появился в 1972 году. А привычка заключать блок кода в скобки, давать переменной тип, собирать разнородные данные в одну именованную структуру — старше его; она пришла из времени, когда самого C ещё не было.
Значит, то, что мы зовём «синтаксисом C», C не изобретал. Он это унаследовал, отшлифовал и закрепил на полвека вперёд. Вопрос, стало быть, не в том, кто придумал скобки, а в том, откуда взялась сама идея, что код может иметь форму, независимую от железа, — и почему именно эта форма пережила всё, что было после неё.
За идеей стоит разрыв. С одной стороны — мышление категориями машины: регистр, адрес, прыжок. С другой — мышление категориями задачи: условие, повторение, результат. Между этими двумя берегами и пролёг первый язык, а весь дальнейший путь окажется медленным переходом от первого ко второму. C встанет ровно посередине — достаточно высоко, чтобы думать, достаточно низко, чтобы управлять.
Это корень дерева. С него всё и растёт.
1.1. Мир до языков: код для машины, не для человека
Чтобы оценить, каким переломом станет идея языка, нужно встать в точку, где её ещё не было.
В начале 1950-х программа не описывала задачу — она перечисляла движения машины. Программист, задумавший вычислить факториал, держал в голове не математическое «перемножить числа от единицы до n», а череду операций над железом: поместить значение в регистр, сравнить его с единицей, перейти по такому-то адресу, если результат меньше, умножить содержимое одной ячейки на содержимое другой, сохранить итог туда-то. Код был списком приказов конкретному процессору — на его наречии, в его терминах, под его устройство. Между намерением человека и работой машины не стояло ничего: программист сам спускался на уровень железа и говорил с ним напрямую.
Это был разговор без переводчика. Числовые коды операций, номера регистров, адреса ячеек, условные и безусловные переходы — словарь, в котором попросту не было слова для замысла. У величин не было имён, только адреса; у действий — не названия, а номера. Нельзя было спросить машину, чего ты хочешь добиться; можно было лишь продиктовать, что ей делать по шагам. И диктовать приходилось на языке этой и только этой машины — на перфокарте, пробитой под её систему команд.
И запись эта была хрупкой до жестокости. Один неверный адрес — и переход уводил исполнение не туда, куда задумано; программа не падала с внятной жалобой, а тихо принималась портить чужую память или считать мусор как ни в чём не бывало. Не было имени, по которому можно окликнуть ошибку, — был лишь номер ячейки, который надо держать в голове самому. Программист вёл сплошную ведомость всего железа разом: где какое число, какой регистр свободен, какой адрес уже занят. Малейший сбой в этой ведомости рушил всё, и отыскать его удавалось, только пройдя весь маршрут машины шаг за шагом, её собственными глазами. Большая часть умственного труда уходила не на задачу, а на то, чтобы не потерять нить в учёте состояний.
Отсюда цена, которую сегодня трудно вообразить. Программа была сращена с процессором, как партитура, записанная не нотами, а перечнем клавиш одного-единственного рояля: третья слева, потом седьмая, потом снова третья. На этом рояле всё звучит. Перенесите запись на другой инструмент — и она обратится в бессмыслицу, потому что описывала не музыку, а механику конкретного корпуса. Перенести программу на другую машину означало не настроить её, а написать заново. Другой набор регистров, другие коды операций, другая карта памяти — и весь труд начинался с чистого листа. Код не принадлежал задаче. Он принадлежал железу.
Вместе с программой к железу было приковано и умение. Виртуоз одной машины, знавший наизусть её систему команд и повадки её памяти, на другой машине оказывался едва ли не новичком: всё его мастерство держалось на устройстве, которого здесь больше не было. Знание не накапливалось поверх машин — оно испарялось вместе с ними. Каждое новое железо требовало не просто переписать программы, но заново выучить, как вообще на нём думать. Опыт не переносился, потому что был опытом не о задачах, а о конкретном механизме. Один и тот же давно понятый алгоритм человечество выводило заново снова и снова — столько раз, сколько было разных машин.
А раз так, то и думать приходилось категориями железа. Вопрос «как решить задачу» незаметно подменялся вопросом «как заставить эту машину выдать нужный результат». Любую мысль программист переводил на язык регистров и адресов ещё прежде, чем успевал её толком додумать, — и чем сложнее становилась задача, тем больше внимания уходило не на неё, а на бухгалтерию машинных состояний: что сейчас лежит в каком регистре, не затёрта ли нужная ячейка, куда указывает очередной переход. Замысел о задаче тонул в учёте механики. Человек обслуживал машину, а не машина — человека.
Код был прикован к железу короткой цепью. Между мыслью человека и работой процессора не стояло никакого посредника — только сама машина, и говорить с ней дозволялось исключительно на её условиях. Не код приближался к замыслу; замысел спускался к коду, разбирался на машинные движения и в них растворялся.
И вот что в этой картине важнее всего: тогда ещё не существовало идеи, что код может быть чем-то иным. Не было представления, что программа способна выражать намерение, а не диктовать движения. Машинный код не казался несовершенным или временным — он был единственным мыслимым способом говорить с вычислителем. Нельзя тосковать по абстракции, о которой не подозреваешь; нельзя счесть запись «слишком низкой», если других этажей ещё никто не придумал. Программирование держалось за то, что под рукой, не оттого, что было примитивным, а оттого, что вертикали попросту не было — подниматься было некуда.
Это и есть нулевая точка. Не первая ступень лестницы, а земля, на которой лестница ещё не построена. Всё, что случится дальше, — полвека языков, поколения создателей, бесконечные споры о компромиссах — будет одним непрерывным движением в одну сторону: прочь от машины, ближе к мысли. Каждый следующий шаг отдалит код от регистров и адресов и придвинет его к тому, что человек на самом деле хочет сказать. Сначала у ячеек появятся имена вместо номеров, и память перестанет быть голой россыпью адресов. Потом вместо череды операций можно будет записать выражение — и машина сама разберётся, в каком порядке его считать. Потом разрозненные данные соберутся в структуру, у действий появятся названия, у повторения — форма, не зависящая от того, как именно устроен переход внутри процессора. Расстояние между намерением и его записью будет сокращаться десятилетие за десятилетием — и в этом сокращении весь сюжет.
Но чтобы движение началось, кто-то должен был первым высказать ересь: что код описывает не машину, а задачу. Что между человеком и процессором может встать посредник, говорящий на языке алгоритма, а не железа, — и что писать, обращаясь к этому посреднику, окажется не только удобнее, но и правильнее, потому что задача переживёт любую машину, на которой её однажды решали.
1.2. ALGOL — латынь программирования
У ереси появились имя и дата. В 1958 году международная группа математиков и инженеров собралась не затем, чтобы приручить очередную машину, а затем, чтобы записать алгоритм как таковой — безразлично к тому, на каком железе ему потом исполняться. Через два года, в 1960-м, эта работа отлилась в язык, вошедший в историю под именем ALGOL. Его создавала не фирма под свой процессор, а сообщество, и сама эта отвязанность от производителя была заявлением: язык принадлежит задаче, а не машине. Впервые код описывал, что нужно вычислить, а не какими движениями этого добиться на конкретном устройстве.
Что значит «описывать алгоритм»? Это значит, что в тексте программы появляется структура, отсутствовавшая в машинном коде, — структура самой мысли. Действие, которое логически едино, можно очертить как единый блок, отделив его от остального явной границей. Блоки можно вкладывать друг в друга, как вложены друг в друга рассуждения: внутреннее живёт в области видимости внешнего и не протекает наружу. У величин появляются типы — обещание того, какого рода значение здесь хранится, данное заранее, а не выясняемое по факту. А процедура может вызвать саму себя, потому что речь идёт не о прыжке по адресу, а о понятии, определённом через себя. Ничего этого нет в списке приказов процессору. Всё это есть в человеческом рассуждении — и ALGOL впервые перенёс это в код. А ещё — и это, быть может, тише всего прозвучало тогда и громче всего отозвалось потом — такой текст впервые стало возможно прочесть. Программу на ALGOL мог понять человек, в глаза не видевший машины, на которой ей предстояло работать: смысл держался в структуре записи, а не в знании железа. Код перестал быть личной перепиской программиста с процессором и стал текстом, обращённым в том числе к другому человеку.
Была у него и черта, которую легко недооценить. Грамматику ALGOL описали строго и формально — не россыпью примеров, а системой правил, по которым из простых конструкций собираются сложные. Язык, на котором программируют, сам впервые получил язык, на котором его можно точно определить. Это переводило разговор о синтаксисе из области привычки в область математики: отныне можно было доказательно сказать, какая запись правильна, а какая нет, не сверяясь с поведением одной-единственной реализации. Форма кода стала предметом, о котором есть строгое знание.
И вот парадокс, ради которого всё это рассказано. Как рабочий инструмент ALGOL почти не выжил. Он рождался в академической среде, был придирчив к чистоте и равнодушен к прозе производства; в нём долго не находилось общего ответа даже на такой приземлённый вопрос, как ввод и вывод данных, — а без этого язык неудобен там, где надо не рассуждать об алгоритмах, а каждый день что-то считать и печатать. Индустрия повозилась с ним и разошлась по более практичным наречиям. На ALGOL, в строгом смысле, давно никто не пишет. Если мерить языки выживанием их собственных программ, ALGOL — проигравший.
И всё же по другому счёту он победил ещё при жизни. Когда учёному нужно было опубликовать алгоритм — в журнале, в учебнике, в письме коллеге, — он записывал его не на наречии своей машины, которое ничего не сказало бы читателю с другим железом, а на ALGOL. Десятилетиями язык служил способом сообщить алгоритм человеку, а не предписать его машине: на нём печатали, разбирали, обсуждали процедуры, которые в этом самом виде, возможно, никогда и не запускали. ALGOL стал тем, на чём думали и договаривались, — общей записью, понятной поверх любого железа и любого десятилетия. Уже это делало его не инструментом, а языком в полном смысле слова.
Но есть и третья мера. Латынь тоже мёртвый язык: на ней не торгуются на рынке и не бранятся в очереди, у неё не осталось носителей, для которых она родная. И всё же она не исчезла — она растворилась. Её грамматика держит изнутри половину европейских языков; её падежи, её логика согласования, её корни проступают в итальянском, французском, испанском так, что, выучив один из них, начинаешь угадывать соседний. Латынь не та, на которой говорят, а та, из которой говорящие языки скроены.
ALGOL — латынь программирования ровно в этом смысле. Его собственное тело умерло, но его грамматика оказалась бессмертной. Идея явного блока, идея вложенности и областей видимости, идея типизированных величин, идея языка как формально описанной системы — всё это не похоронили вместе с языком, а унаследовали. Каждый следующий язык, всерьёз претендовавший на промышленную жизнь, строился на этой грамматике, даже когда полемизировал с ней. Программист, никогда не видевший ни строчки на ALGOL, ежедневно говорит на его наследниках и сам того не знает — как человек, не учивший латынь, всё же склоняет слова по её давно растворившимся правилам.
Это и есть корень дерева. Не самая толстая ветвь, не самый заметный ствол — а то, что под землёй, чего не видно сверху и без чего не стоит ничего. ALGOL дал не язык, на котором будут писать, а форму, в которую отольются все языки, на которых будут писать. Он задал грамматику раньше, чем нашёлся словарь; предложил скелет раньше, чем появилось тело.
В этом первый намёк на то, что станет ясно много позже: судьба языка и судьба заложенной в нём идеи — две разные судьбы. Инструмент может устареть, проиграть в удобстве и кануть, не пережив своего десятилетия, — а идея, которую он первым высказал, продолжит жить в чужих телах, набирая силу с каждым новым воплощением. ALGOL умер как программа и остался как грамматика. Это не парадокс и не случайность — это правило, по которому здесь всё устроено.
Грамматике, чтобы жить, нужно тело. Латынь стала бессмертной не сама по себе, а через языки, на которых заговорили живые народы с их базарами, законами и любовными письмами. ALGOL остался бы изящной академической древностью, музейным чертежом грамматики, если бы его форму не подхватил кто-то, кому нужно было не рассуждать об алгоритмах, а строить работающую систему здесь и сейчас, под давлением реальной задачи и реального железа. Такой человек нашёлся — и пришёл он не из комитета, а из машинного зала.
1.3. Ритчи и C — язык, определивший полвека
Он пришёл не из комитета, а из машинного зала. На рубеже 1970-х в Bell Labs небольшая группа инженеров строила операционную систему, которой предстояло называться Unix, и упёрлась в стену, знакомую всякому, кто писал под конкретное железо: система, собранная на ассемблере одной машины, к этой машине и оставалась прикована. Перенести её на другую означало переписать всё заново — ту самую цену система платила в полной мере. Деннису Ритчи нужен был язык, на котором операционную систему можно написать однажды, а запускать на разных машинах. Не изящный, не академический, не образцовый — рабочий.
Грамматику он изобретать не стал. От ALGOL через несколько промежуточных языков тянулась линия, дошедшая до совсем аскетичного B, на котором экспериментировал рядом Кен Томпсон, — и Ритчи взял эту линию и дал ей тело, годное для системной работы. Он добавил то, чего B недоставало для серьёзных программ, прежде всего типы, и подогнал форму под суровую практику: компилятор должен быть простым, код — предсказуемым, язык — близким к железу настолько, чтобы на нём имело смысл писать операционную систему. В 1972 году из этого вышел C.
Решения, которые он принял, не выглядели судьбоносными. Блок кода Ритчи заключил в фигурные скобки. Условие записал через if, повторение — через for и while, возврат значения — через return. Разнородные данные позволил собрать в одну именованную единицу — struct. Дал прямой доступ к памяти через указатели и неглубокую, аккуратную систему типов. Каждое из этих решений можно было принять иначе; в других языках той поры их и принимали иначе. Но прижились именно эти, ритчевские, — и не на год, а на полвека. Спустя десятилетия мы по-прежнему очерчиваем блок фигурными скобками и выходим из функции словом return, и зовём это «синтаксисом C», хотя добрая половина этих решений старше самого C, а другая половина могла бы быть какой угодно.
В этих решениях сквозит не эстетика, а экономия. Фигурная скобка коротка и не требует от компилятора почти ничего; типы Ритчи держал неглубокими, чтобы язык оставался простым в реализации; форму правил так, чтобы её легко было перенести на новую машину вслед за Unix. Красота здесь ни при чём — есть инженерная бережливость человека, которому важнее, чтобы работало и переносилось, чем чтобы восхищало. Половину «того самого синтаксиса» продиктовала не теория языка, а скупость практика.
Почему прижились именно они? Не потому, что были доказуемо лучшими, — а потому, что пришли не одни. C явился в мир не предложением на бумаге, а с уже написанной на нём главной программой эпохи. Unix переписали на C — и операционная система впервые перестала быть пленницей одного процессора. Чтобы перенести её на новую машину, теперь переписывали не систему, а компилятор; всё остальное переезжало почти без изменений. Проклятие машинного кода — «другая машина значит весь труд заново» — было снято. А вместе с Unix по университетам и вычислительным центрам расходился язык, на котором та была написана. Кто получал систему — получал и C; кто правил систему — учил C. Язык распространялся не уговорами, а тяжестью того, что на нём уже стояло.
Дальше это лишь нарастало. Операционные системы стали писать на C почти по умолчанию; на нём поднялась та невидимая инфраструктура, на которой потом заработало всё прочее. Языки, пришедшие следом, нередко и сами были сперва написаны на C, обращались к внешнему миру через его соглашения и в конечном счёте компилировались в тот же машинный код тем же путём. C перестал быть просто популярным языком и стал полом, на котором стоят остальные, — настолько привычным, что его уже не замечают, как не замечают фундамент, пока тот держит.
И здесь видно, чем C был по сути. Он встал мостом между двумя берегами, которые до него не сходились. С одной стороны — наследие ALGOL: блоки, функции, типы, именованные структуры, возможность думать категориями задачи. С другой — близость к железу, какой академические языки сторонились: указатель, напрямую адресующий память, операции, ложащиеся на машинные команды чуть ли не одна к одной, отсутствие тяжёлого посредника между текстом и процессором. На C можно было рассуждать об алгоритме — и одновременно чувствовать металл под пальцами. Достаточно высоко, чтобы мыслить; достаточно низко, чтобы управлять.
Эту двойственность можно было пощупать в каждой строке. Глядя на выражение на C, опытный человек примерно представлял, в какие машинные команды оно обратится: язык почти ничего не прятал, и в нём угадывался процессор. И при этом то же выражение читалось как мысль о задаче, а не как перечень приказов железу. C не уводил от машины так далеко, чтобы потерять её из виду, и не прижимал к ней так близко, чтобы за регистрами пропала задача. Его потому и прозвали портативным ассемблером — полушутя, но метко: вся прямота ассемблера при способности думать поверх отдельной машины.
За этим компромиссом стояло убеждение, тихое и оттого прочное: программисту можно доверять. C не страховал, не запрещал, не отводил руку — он давал полный доступ и отступал в сторону, полагая, что человек, которому вручили власть над памятью, распорядится ею с умом. В этом была честность инструмента, не притворяющегося умнее своего хозяина. В этом же была и приоткрытая дверь, в которую следующие полвека станут входить один за другим те, кто решит, что доверие стоило бы и ограничить. Но это уже их убеждения, не Ритчи; его собственное было именно таким, и язык получился по его образу.
Так «синтаксис C» сделался тем самым синтаксисом. Не потому, что он совершенен, — он не совершенен, — а потому, что встал ровно посередине: достаточно абстрактным, чтобы на нём думали, достаточно конкретным, чтобы ему доверили машину, и достаточно вовремя, чтобы прийти вместе с системой, которую захотели все. Грамматика ALGOL обрела наконец живое тело, и тело это зажило долго.
Насколько долго — лучше видно не в рассуждении, а глазами. Если решения Ритчи и вправду стали общим фундаментом, их след должен проступать там, где о самом C давно не вспоминают: в языках, родившихся через двадцать, тридцать, сорок лет после него, в чужих культурах и на других материках смысла. Чтобы это увидеть, довольно положить их рядом.
1.4. ДНК, которая живёт 50 лет
Вот одна и та же задача — вычислить факториал — записанная на четырёх языках, между которыми лежит больше сорока лет.
C, 1972:
int factorial (int n) { if (n <= 1) return 1; return n * factorial (n — 1); }Java, 1995:
int factorial (int n) { if (n <= 1) return 1; return n * factorial (n — 1); }Go, 2009:
func factorial (n int) int { if n <= 1 { return 1 } return n * factorial (n — 1) }Rust, 2015:
fn factorial (n: u64) -> u64 { if n <= 1 { return 1; } n * factorial (n — 1) }Четыре языка. Четыре эпохи: начало семидесятых, середина девяностых, конец нулевых, середина десятых. Четыре разные культуры со своими ценностями и своими спорами, рождённые в разных компаниях, разными людьми, ради разных целей. И всё же если приглядеться, видно, что бо́льшая часть этого кода — попросту одно и то же.
Тело функции у всех четверых заключено в фигурные скобки. У всех — функция с типизированным параметром на входе и типом результата на выходе. У всех ветвление сделано через if, выход — через return, а само вычисление — умножение на результат рекурсивного вызова — совпадает до знака. Это не похожие решения похожей задачи. Это один и тот же чертёж, перерисованный четыре раза. По прикидке — процентов восемьдесят символов несут одну и ту же мысль одной и той же конструкцией.
И стоит заметить, что именно совпало. Не случайные мелочи — а ровно тот костяк, что собирался на наших глазах: блок в скобках, вложенность, типы пришли от ALGOL; их закрепление и распространение — от C. То, что заложили между 1958 и 1972 годом, и есть та часть, которой четыре языка не коснулись. Совпадает не что попало, а фундамент, — и совпадает он именно потому, что фундамент.



