Код. Культура, скомпилированная в байты
Код. Культура, скомпилированная в байты

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

Код. Культура, скомпилированная в байты

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

История типизации – это история поиска баланса между этими полюсами.

Робин Милнер, работая в Эдинбургском университете с 1973 года, искал золотую середину. Он разрабатывал ML – метаязык для системы доказательства теорем LCF. Ему нужен был язык, который был бы надёжен, как статически типизированные языки, но удобен, как динамические. Результатом стала система вывода типов, которая позже получила название Хиндли-Милнера (по имени Милнера и логика Роджера Хиндли, который независимо пришёл к похожим результатам).

Идея была элегантна: компилятор сам определяет типы выражений, анализируя, как они используются. Программисту не нужно писать аннотации – но программа всё равно статически типизирована. Если вы пишете функцию, которая складывает два аргумента, компилятор выведет, что аргументы должны быть числами. Если вы потом попробуете передать ей строку – ошибка компиляции. Но вам не пришлось объявлять типы явно.

Это был революционный компромисс. ML и его потомки – Haskell, OCaml, F# – предлагали статические гарантии без синтаксического бремени. Программист писал код почти как на динамическом языке, наслаждаясь лёгкостью и скоростью. Но компилятор выводил типы и проверял их согласованность, обеспечивая надёжность статической типизации. Лучшее из двух миров.

Современные языки всё чаще выбирают эту модель. Go выводит типы локальных переменных: достаточно написать x:= 5, и компилятор поймёт, что x – целое число. Kotlin делает то же самое, добавляя более мощную систему типов с null safety. Rust сочетает вывод типов с одной из самых строгих систем проверок в индустрии – его borrow checker отслеживает владение и время жизни значений, предотвращая целые классы ошибок памяти.

Swift, созданный Apple в 2014 году, тоже использует вывод типов. Как и Scala, сочетающая объектно-ориентированное и функциональное программирование. Вывод типов стал мейнстримом – не потому, что программисты ленивы, а потому, что он позволяет писать выразительный код без потери безопасности.

Но самое интересное развитие последних лет – постепенная типизация, gradual typing. Это подход, который признаёт: иногда нужна гибкость динамики, иногда – гарантии статики. Почему бы не иметь и то, и другое?

TypeScript добавляет типы к JavaScript, но делает их опциональными. Можно начать с полностью динамического кода – он будет работать. Потом постепенно добавлять аннотации: сначала для публичных API, потом для внутренних функций, потом везде. Строгость нарастает по мере готовности кодовой базы и команды. Это не компромисс – это путь миграции.

MyPy делает то же для Python. Sorbet – для Ruby. Hack, созданный Facebook1, добавил постепенную типизацию к PHP. Dart начинал с опциональных типов, хотя позже перешёл к обязательным. Постепенная типизация оказалась не академическим экспериментом, а практическим инструментом для огромных кодовых баз, которые нельзя переписать за один день.

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

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

Когда работаешь со статическим языком, начинаешь думать типами. Сначала проектируешь структуры данных, потом пишешь функции. Типы становятся документацией: сигнатура функции говорит, что она принимает и что возвращает. IDE использует типы для автодополнения, рефакторинга, навигации по коду. Рефакторинг становится безопасным – компилятор укажет на все места, которые нужно изменить. Типы становятся языком общения между частями системы и между членами команды.

Когда работаешь с динамическим языком, думаешь значениями. Что здесь лежит прямо сейчас? Как это преобразовать? Тесты заменяют статическую проверку. Duck typing – «если что-то ходит как утка и крякает как утка, это утка» – позволяет писать обобщённый код без формального описания интерфейсов. Не важно, какого типа объект. Важно, что он умеет делать. Это другой способ думать о программах – более гибкий, более ситуативный, более близкий к тому, как работает реальный мир.

Ни один подход не лучше другого. Они отвечают на разные вопросы, решают разные проблемы, подходят для разных ситуаций.

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

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

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

Каждый ответ имеет свою логику. Каждый создаёт свою культуру программирования.

2.3. Обработка ошибок как отношение к неудаче

Ошибки неизбежны.

Файл не найден. Сеть недоступна. Пользователь ввёл текст вместо числа. Память закончилась. Диск заполнен. Соединение разорвано. Сервер не отвечает. Данные повреждены.

Как язык программирования предлагает справляться с неизбежным? Ответ на этот вопрос многое говорит о философии языка. Это не техническая деталь – это мировоззренческая позиция.

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

Это подход, рождённый из минимализма. Никаких специальных механизмов, никакой магии, никакого скрытого потока управления. Ошибка – это просто значение, как и любое другое. Функция вернула -1? Это значение. Что с ним делать – решает вызывающий код. Может проверить. Может проигнорировать. Может передать дальше.

Но это требует дисциплины. Легко забыть проверку. Легко проигнорировать возвращённое значение. Компилятор не напомнит. Си доверяет программисту, и программист иногда подводит. История компьютерной безопасности полна уязвимостей, возникших из-за непроверенных кодов возврата.

Go унаследовал этот подход, но сделал его более явным и более настойчивым. Функции в Go могут возвращать несколько значений, и идиоматический паттерн – возвращать результат и ошибку вместе:

result, err:= someFunction ()if err!= nil {    return err}

Эти три строчки – if err!= nil – повторяются в Go-коде десятки, сотни раз. Они стали мемом, предметом шуток и критики. Противники Go называют это многословием, шумом, церемонией, которая засоряет код и отвлекает от сути.

Создатели Go видят в этом добродетель.

«Errors are values», – написал Роб Пайк в статье 2015 года. Ошибки – это значения. Не исключительные ситуации, не что-то особенное, а обычные значения, с которыми можно работать, которые можно передавать, комбинировать, оборачивать, анализировать. Философия Go в том, что ошибки – нормальная часть потока выполнения. Они не исключительны. Они ожидаемы. И код должен явно показывать, как с ними обращаются.

Когда вы видите if err!= nil в коде на Go, вы видите место, где программист подумал об ошибке. Принял решение. Обработал или передал дальше. Это не скрыто где-то в стеке вызовов, не спрятано в блоке catch на другом конце файла. Это здесь, на виду, часть основного потока кода.

Философия исключений, реализованная в Java, Python, C#, C++, противоположна. Исключения – это механизм для ситуаций, которые не должны происходить в нормальном потоке. Файл должен открыться. Сеть должна работать. Память должна выделиться. Если что-то пошло не так – это исключение из нормы. Выбрасываем объект исключения, и он «всплывает» по стеку вызовов, пока кто-то его не поймает.

Эта модель позволяет писать «счастливый путь» – код, который описывает, что происходит, когда всё хорошо. Обработка ошибок отделена от основной логики, вынесена в блоки try-catch. Основной код становится чище, намерение – яснее. Вот что программа делает. А вот, отдельно, что происходит, если что-то идёт не так.

Но есть цена.

Исключения создают скрытый поток управления. Глядя на строку кода, не всегда понятно, может ли она выбросить исключение. Функция вызывает другую функцию, та – третью, и где-то в глубине происходит ошибка. Исключение летит через десять уровней стека, пока не найдёт обработчик. Или не найдёт – и программа упадёт.

Легко забыть, какие исключения может выбросить функция. Легко пропустить ошибку, которая тихо поднимется наверх и уронит программу в неожиданном месте. Java пыталась решить эту проблему через checked exceptions – обязательное объявление исключений в сигнатуре функции. Но программисты возненавидели это и научились обходить: ловить исключение, оборачивать в RuntimeException, выбрасывать снова. Добровольно-принудительная проверка оказалась хуже, чем никакой проверки.

Rust и Haskell предлагают третий путь – алгебраические типы для ошибок. Тип Result в Rust может содержать либо успешное значение Ok, либо ошибку Err. Тип Option может содержать значение Some или ничего None. И компилятор требует обработать все случаи.

Это кажется похожим на коды возврата, но есть критическое отличие: компилятор не позволит игнорировать ошибку. Если функция возвращает Result, программист обязан что-то с ним сделать. Обработать оба варианта через pattern matching. Или явно передать ошибку выше с помощью оператора?. Или явно проигнорировать с помощью unwrap – но тогда программа упадёт с паникой, если ошибка произойдёт. Каждый выбор явен. Каждый выбор – осознанное решение.

Философия Rust: ошибки – это данные, у которых есть тип. Не исключения, которые могут вылететь откуда угодно. Не коды, которые легко проигнорировать. Структурированные данные, которые компилятор умеет отслеживать. Ошибка становится частью сигнатуры функции, частью контракта. Глядя на тип функции, вы знаете, может ли она завершиться неудачей и какого рода неудачей.

Но есть и радикально иной подход – философия «пусть падает», рождённая в Erlang.

Erlang был создан в Ericsson в середине 1980-х для телекоммуникационных систем. Телефонные станции должны работать всегда. Не «почти всегда». Не «99% времени». Всегда. Требование было сформулировано как «пять девяток» – 99.999% доступности, что означает не более пяти минут простоя в год. А в некоторых случаях говорили о «девяти девятках» – 99.9999999%, что означает доли секунды простоя за десятилетия.

Как достичь такой надёжности? Джо Армстронг, один из создателей Erlang, пришёл к парадоксальному выводу.

«Мы были, кажется, единственными людьми в мире, проектировавшими систему, которая могла бы восстанавливаться после программных ошибок», – вспоминал он. Команда Erlang постоянно задавала один и тот же вопрос: «Что произойдёт, если это сломается?» И почти всегда получала ответ: «Наша модель предполагает отсутствие сбоев».

Армстронг понял: нельзя предотвратить все ошибки. Программы пишут люди, люди ошибаются. Оборудование выходит из строя. Сеть рвётся. Вместо того чтобы пытаться предотвратить каждую возможную ошибку – что невозможно – нужно строить систему, которая выживает при ошибках.

Отсюда философия «let it crash» – пусть падает. Не пытайся обработать каждую ошибку. Пусть процесс упадёт. Другой процесс – супервизор – заметит падение и перезапустит упавший. Процессы изолированы друг от друга, не делят память, общаются только сообщениями. Падение одного не затрагивает другие.

Это требует особой архитектуры: тысячи лёгких изолированных процессов, деревья супервизоров, отсутствие разделяемого состояния. Но в такой архитектуре код становится проще. Не нужно писать защитный код для каждой возможной ошибки. Пиши «счастливый путь» – что программа должна делать. Если что-то пошло не так – процесс умрёт и возродится в чистом состоянии.

Армстронг описывал это так: представьте идеальную организацию, где каждый сотрудник делает свою работу. Если сотрудник не справляется, его увольняют и нанимают нового. Менеджеры следят за сотрудниками и при необходимости заменяют их. Директора следят за менеджерами. И так далее. Система остаётся работоспособной, даже если отдельные части выходят из строя.

Четыре философии, четыре ответа на один вопрос.

Си и Go говорят: ошибки – нормальная часть жизни, проверяй их явно, каждый раз. Это дисциплина, это внимательность, это ответственность программиста.

Java и Python говорят: ошибки – исключения из нормы, обрабатывай их отдельно, чтобы не засорять основной код. Это разделение ответственности, это чистота намерения, это фокус на «счастливом пути».

Rust и Haskell говорят: ошибки – структурированные данные, пусть компилятор следит, чтобы ты их не пропустил. Это безопасность через типы, это гарантии на этапе компиляции, это невозможность забыть.

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

Каждый подход формирует код и мышление.

Программист на Go привыкает видеть обработку ошибок на каждом шагу – это часть текстуры кода, как знаки препинания в тексте. Программист на Python привыкает думать об исключениях как о потоке, который идёт параллельно основному, – иногда пересекаясь с ним в блоках try-catch. Программист на Rust привыкает к тому, что компилятор не даст забыть об ошибке, – это строгий, но справедливый учитель. Программист на Erlang привыкает думать о системах, не о функциях – о том, как части взаимодействуют и как восстанавливаются после сбоев.

Выбор модели обработки ошибок – это выбор отношения к неудаче. Неудача – это то, что нужно проверять постоянно? Или исключительное событие? Или типизированные данные? Или нормальная часть жизни системы?

Ответ определяет не только синтаксис. Он определяет архитектуру. Он определяет культуру программирования на языке. Он определяет, как программисты думают о надёжности и что считают «правильным» кодом.

Синтаксис как мировоззрение

Мы рассмотрели три аспекта синтаксиса: границы блоков, типизацию, обработку ошибок. В каждом случае технические решения оказывались философскими.

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

Синтаксис – это не косметика языка. Это его мировоззрение, отлитое в форму.

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

Программист на Haskell видит мир как композицию чистых функций, как поток преобразований данных. Программист на Java видит мир как иерархию объектов, как взаимодействие сущностей с состоянием и поведением. Программист на Си видит мир как последовательность операций над памятью, как байты и указатели. Программист на Erlang видит мир как сеть процессов, обменивающихся сообщениями.

В этом – сила и ограничение языков программирования. Они дают нам инструменты мышления. Мощные инструменты. Но эти инструменты формируют мысль. Молоток видит везде гвозди. Функциональный программист видит везде функции. Объектно-ориентированный – везде объекты.

Ни одна картина не полна. Ни одна не единственно верна. Но каждая по-своему когерентна, по-своему красива, по-своему продуктивна. Каждая позволяет решать определённые задачи элегантно – и делает другие задачи неуклюжими.

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

Код – это культура, выраженная в синтаксисе.

Глава 3. Имена и метафоры

Программист пишет код для машины, но читает его человек. И первое, с чем сталкивается читающий – имена. Имена переменных, функций, классов, модулей. За каждым именем стоит решение, за каждым решением – философия.

Выбор ключевого слова для объявления функции может показаться мелочью. Какая разница – def, func, fn или function? Машине действительно всё равно. Но человеку – нет. Имена формируют то, как мы думаем о коде. А конвенции именования – тот невидимый каркас, который держит программы читаемыми десятилетия спустя после их написания.

Выбирая имена, программист совершает акт классификации. Он решает, к какой категории принадлежит сущность, какие её свойства существенны, какие – нет. Имя переменной temp говорит: это временное, не обращай внимания. Имя customerLifetimeValue говорит: это важно, это центральная концепция бизнеса. Между этими полюсами – бесконечный спектр решений, каждое из которых отражает понимание программистом задачи.

3.1. Ключевые слова как концептуальный выбор

В 1967 году в Норвежском вычислительном центре Оле-Йохан Даль и Кристен Нюгор работали над языком для симуляции дискретных событий. Им нужно было слово для обозначения шаблона, по которому создаются объекты симуляции. Они выбрали class. Позже Даль вспоминал: «Мы выбрали термины class и objects of classes для нашей новой Simula. Понятие подкласса было особенно привлекательным для нас».

Это был не просто технический выбор. Слово «класс» несёт в себе идею классификации, таксономии, иерархии. Классы предполагают, что мир можно разделить на категории, что объекты принадлежат к определённым типам, что есть родовые понятия и видовые различия. Когда программист объявляет class Animal, он невольно начинает думать категориями Аристотеля.

Влияние этого выбора оказалось колоссальным. Simula повлияла на Smalltalk, Smalltalk – на C++, C++ – на Java, Java – на C#, C# – на всё остальное. Термин «класс» стал настолько привычным, что программисты перестали замечать метафору, которую он несёт. Но метафора продолжает работать. Иерархии наследования, которые программисты строят в объектно-ориентированных языках, отражают таксономическое мышление, заложенное в самом слове.

Альтернатива – struct. В языке Си структура – это просто способ группировки данных. Никакой иерархии, никакого наследования, никакого поведения. Данные и только данные. Когда Деннис Ритчи в 1972 году выбирал это слово, он думал о памяти, о байтах, о том, как данные расположены физически. Структура – это карта участка памяти, не более того.

Си создавался для написания операционных систем. Ритчи и Томпсон переписывали Unix, им нужен был язык, который позволял бы контролировать железо так же точно, как ассемблер, но был бы более выразительным. Структуры в Си – это не абстракции, скрывающие реализацию. Это способ сказать компилятору: вот здесь лежат эти байты, а вот здесь – эти. Программист, работающий со структурами Си, думает о памяти, не о таксономиях.

Go, созданный спустя почти сорок лет, сознательно отказался от классов в пользу структур. Роб Пайк и его коллеги не хотели, чтобы программисты мыслили иерархиями наследования. В Go нет слова class, есть только struct и interface. Это не ограничение языка – это философское заявление. Композиция вместо наследования. Поведение определяется тем, что объект делает, а не тем, от какого предка он происходит.

Пайк неоднократно объяснял это решение. Иерархии наследования в больших проектах становятся хрупкими и сложными для понимания. Изменение базового класса может сломать десятки наследников непредсказуемым образом. Go предлагает альтернативу: встраивание одних структур в другие и интерфейсы, которые определяются поведением, а не наследованием. Утиная типизация: если объект ходит как утка и крякает как утка – для Go он утка, независимо от того, кто его родители.

Ключевые слова для объявления функций демонстрируют тот же спектр философий. Python использует def – сокращение от define, определить. Нейтральное, техническое слово. Вы определяете функцию, как определяете переменную. Никакой особой церемонии. Это соответствует общей философии Python: минимум синтаксического шума, максимум ясности.

Гвидо ван Россум выбрал def под влиянием ABC – образовательного языка, созданного в том же голландском исследовательском центре CWI, где он работал. ABC был спроектирован для обучения программированию, и каждое его решение оптимизировалось для понятности новичкам. Python унаследовал эту заботу о читаемости, и def – часть этого наследия.

Go пишет func – то же сокращение, та же нейтральность, но ещё лаконичнее. Четыре символа вместо пяти. В языке, где простота возведена в принцип, каждый символ на счету. Go избегает сокращений ради сокращений, но func оправдан частотой использования. Функции в Go пишутся постоянно, и экономия одного символа на каждой из них складывается в заметное уменьшение визуального шума.

Rust идёт ещё дальше: fn. Два символа. Минимум возможного. Это не небрежность – это сознательный выбор для языка, где функции пишутся постоянно и везде. Когда пользователи спрашивали, почему не полное слово function, разработчики отвечали ссылками на традицию системного программирования, где краткость ценится выше явности. Rust наследует культуру Си и Unix, где имена короткие и ёмкие.

Конец ознакомительного фрагмента.

Текст предоставлен ООО «Литрес».

Прочитайте эту книгу целиком, купив полную легальную версию на Литрес.

Безопасно оплатить книгу можно банковской картой Visa, MasterCard, Maestro, со счета мобильного телефона, с платежного терминала, в салоне МТС или Связной, через PayPal, WebMoney, Яндекс.Деньги, QIWI Кошелек, бонусными картами или другим удобным Вам способом.

Примечания

1

Сервис принадлежит организации, деятельность которой запрещена на территории РФ; здесь и далее.

Конец ознакомительного фрагмента
Купить и скачать всю книгу
На страницу:
3 из 3