Я занимаюсь изучением синтаксиса языков программирования и пытаюсь отделить фундаментальные закономерности от исторических случайностей. Одна из таких случайностей - почти повсеместное использование терминов выражение (expression) и инструкция (statement) при описании и классификации синтаксиса языков программирования.Интуитивно кажется, что 2 + 2 и if (x) { foo(); } - это совершенно разные сущности, но при более глубоком анализе оказывается, что подобное разделение искусственное и возникло из-за архитектурных особенностей вычислительных машин почти полвека назад и с тех пор просто «переходит» из языка в язык. Читать далее
Я занимаюсь изучением синтаксиса языков программирования и пытаюсь отделить фундаментальные закономерности от исторических случайностей. Одна из таких случайностей - почти повсеместное использование терминов выражение (expression) и инструкция (statement) при описании и классификации синтаксиса языков программирования.
Интуитивно кажется, что 2 + 2 и if (x) { foo(); } - это совершенно разные сущности, но при более глубоком анализе оказывается, что подобное разделение искусственное и возникло из-за архитектурных особенностей вычислительных машин почти полвека назад и с тех пор просто «переходит» из языка в язык.
Чтобы понять, откуда взялось разделение синтаксиса на выражения (expression) и инструкции (statement), нужно проследить эволюцию синтаксических правил от первых языков высокого уровня.
FORTRAN -> ALGOL 60 -> C: три акта одной пьесыFORTRAN (1957) - разделение «по железу». В языке, созданном для IBM 704, выражения (X + Y*Z) были строго отделены от управляющих инструкций (IF, DO, GOTO). Первые вычисляли значения, вторые управляли потоком исполнения и смешивать их между сбой было нельзя. Эта концепция синтаксиса напрямую отражала архитектуру вычислительных машин того времени: программа понималась как последовательность машинных команд, каждая из которых либо вычисляла операнд, либо изменяла счётчик команд, но не одновременно.
ALGOL 60 (1960). В ALGOL 60 ввели составной оператор begin ... end (прообраз блока), но важнее другое: язык разрешил условное выражение. Конструкция if B then E1 else E2 могла появляться в позиции любого выражения, а значит, допускала запись
x := if a > b then a else b
Это был прямой предшественник тернарного оператора, показавший, что выбор между двумя значениями прекрасно вписывается в роль выражения.
C (1972) - фиксация различий Expression vs Statement. Деннис Ритчи (при участии Кена Томпсона, автора языка-предшественника B), проектируя C как «переносимый ассемблер» для PDP-11, закрепил в грамматике чёткое различие между этими понятиями. Такое решение диктовалось стремлением к максимально прямому отображению конструкций языка на машинные инструкции: условный переход jz BEQ процессора - это действие, а не способ вычислить значение.
Почти синхронно с FORTRAN, в 1958 году, Джон Маккарти создал Lisp, фундаментально основанный на лямбда-исчислении. Здесь нет понятия statement - вся программа состоит из S-выражений: будь то вызов функции, условная конструкция или блок.
В Лиспе конструкция (if (> a b) a b) - это просто выражение, значение которого можно присвоить, передать в функцию или использовать в более сложной композиции. А блок (let ((x 5)) (+ x 1)) возвращает 6, оставаясь обычным выражением.
Это не «расширение» возможностей, а фундаментальный иной принцип: вся программа - это вычисляемые значения, а не последовательность управляющих инструкций. С этой позиции разделение на expression и statement выглядит ненужным.
Интересно посмотреть, насколько глубоко в современных языках укоренилась граница между этими терминами и как она преодолевается.
Классическая модель: C, C++, Java (и Python)В этих языках if, for, while категорически не могут быть частью выражения. Код
int y = if (x > 0) { 1; } else { 2; } // ошибка компиляции
недопустим. Разработчики вынуждены либо использовать тернарный оператор:
int y = (x > 0) ? 1 : 2;
либо (в C/C++) прибегать к нестандартным расширениям вроде GCC statement expressions:
int y = ({ if (x > 0) 1; else 2; }); // только с расширениями GCC
В Python ситуация аналогична: if - это инструкция, но начиная с версии 2.5 введено тернарное выражение:
y = 1 if x > 0 else 2
Эта конструкция - точный аналог тернарного оператора ?:, «заплатка» для грамматики, которая не решилась сделать if/else полноценным выражением.
JavaScript формально сохраняет C-подобное разделение, однако в язык встроены обходные инструменты. Используя IIFE (немедленно вызываемые функциональные выражения), разработчик может «обернуть» statement-логику в контекст, где требуется значение:
let y = (function() {
if (x > 0) return 1;
else return 2;
})();
До появления современных JIT-оптимизаций подобный трюк нёс реальные накладные расходы на вызов функции, но он наглядно демонстрирует ручную эмуляцию expression-грамматики поверх statement. Показательно, что раз сообщество идёт на подобные ухищрения - значит, потребность в таком подходе велика.
Ruby: всё есть выражениеВ Ruby любая конструкция - выражение, без исключений. Можно написать:
y = if x > 0
1
else
2
end
z = case value
when 1 then "one"
when 2 then "two"
else "other"
end
w = while false
# тело не выполняется
end # w равно nil, но сам while - валидное выражение
Даже определение класса или метода возвращает значение последнего выражения. Грамматика Ruby не вводит отдельного нетерминала «statement» - вся программа состоит только из выражений.
Rust: гибрид - ни нашим ни вашимRust предлагает своеобразный компромисс: формально в грамматике различие между statement и expression есть (let-декларации, объявления элементов - это не expression), но управляющие конструкции (if, match, loop) целиком отнесены к категории Expression - в отличие от C/C++, где это принципиально невозможно.
При этом в Rust есть немало запутанных правил вокруг конструкций, которые вроде бы expression, но ведут себя странно в зависимости от контекста. Например:
let x = while true { break 5; }; // ОШИБКА! while не умеет так
тогда как очень похожий loop - умеет:
let x = loop { break 5; }; // ОК! x == 5
Различие между loop и while/for в том, что компилятор не может гарантировать, что тело while/for вообще выполнится хоть раз (условие может быть ложным с самого начала), тогда как loop либо бесконечен, либо выходит через break с конкретным значением. Эта асимметрия поведения чисто техническая, но принципиально влияет на синтаксис этих конструкций.
Или вот if со скобками и без них ведёт себя неожиданно:
fn f() -> i32 {
if x > 0 { 1 } else { 2 } - 1
}
Кажется, что это должно вычислить (if...) - 1. Но нет! Компилятор трактует if {...} else {...} как отдельный statement (потому что он стоит не в «хвостовой» позиции блока), а - 1 - как отдельное выражение - унарный минус. Функция вернёт ошибку типов, потому что последней строкой оказывается -1, а значение if-выражения будет просто отброшено.
Чтобы получить ожидаемое поведение, нужно явно обернуть if в скобки:
let y = (if x > 0 { 1 } else { 2 }) - 1; // так работает как надо
Но самым странным является поведение точки с запятой, которая может менять тип функции:
fn f() -> i32 {
5 + 3; // с точкой с запятой!
10
}
А если поставить ; после последней строки:
fn f() -> i32 {
5 + 3;
10; // теперь тут ;
}
// ОШИБКА: функция должна вернуть i32, а вернула ()
; (точка с запятой) в Rust - это не просто «конец строки», а оператор, буквально меняющий тип выражения на () (пустой тип). Забытая или лишняя точка с запятой - самая частая причина неочевидных ошибок среди новичков.
Rust действительно продвинулся дальше C в сторону «всё есть expression», но это не единый принцип, а набор точечных, местами противоречащих друг другу правил, каждое из которых решает конкретную инженерную задачу (типовую безопасность, гарантии терминации, разрешение неоднозначности парсинга), а не следствие одной красивой идеи, последовательно применённой везде.
Часть 3. Что на самом деле отличает Statement от Expression?Если вернуться к изначальному вопросу, так чем же отличается statement от expression?
«Statement - выполнение, Expression - вычисление» - оба термина связаны с вычислительными действиями, разница не в природе операции.
«Statement не возвращает значение» - опровергается Ruby, где всё возвращает значение.
«Statement - отдельная категория грамматики» - это просто описание симптома (того, что в конкретном языке зафиксировано на уровне BNF), а не объяснение принципиальных различий между терминами.
Более продуктивным оказывается взгляд на пару statement/expression как на структурную композицию, в которой:
Expression - неделимая лексическая единица: литерал, идентификатор, вызов функции, математическая операция.
Точка с запятой ; (или её аналоги - перевод строки в Ruby) играет роль оператора-разделителя, который сообщает компилятору: нужно вычислить выражение слева, отбросить его значение и перейти к правой части. Цепочка из нескольких выражений, последовательно соединённых ; и образует statement.
Составной блок, ограниченный фигурными скобками {...} - это способ превратить цепочку из нескольких выражений в единое неделимое выражение. Значением блока становится значение последнего выражения в нём.
Таким образом, statement - это не альтернативная категория синтаксиса, а композиция одного или нескольких выражений, соединённых оператором ;, где значение всей цепочки равно значению последнего элемента. Запись:
expression1;
expression2;
...
expressionN
можно прочитать как (expression1 ; expression2 ; ... ; expressionN), и её значением является значение expressionN. А если нужно оформить такую цепочку как единое выражение, то её помещают в фигурные скобки:
{ expression1; expression2; ...; expressionN }
В языках, поддерживающих такую интерпретацию, блоки естественно встают на место выражений в любом контексте - в правой части присваивания, как аргументы функции, как операнды.
А как же оператор запятая в C/C++?Оператор запятая в C/C++ очень похож на реализацию описанной формальной модели - композицию выражений:
int y = (foo(), bar(), 42); // foo() и bar() вычислены ради побочного эффекта, y == 42
Но есть принципиальное отличие: оператор запятая - это всегда Expression и не может выйти за пределы одного expression-контекста.
int y = foo(), bar(); // Это НЕ comma-expression! Это два объявления (переменной и функции)!
int z = (foo(), bar()); // а вот это - настоящий comma operator, нужны скобки
Без явных скобок оператор , (запятая) в контексте объявления переменных или списка аргументов функции интерпретируется грамматикой иначе - как разделитель в списке (declarator-list, argument-list). И эта неоднозначность грамматики C/C++ разрешается только контекстом.
Кроме того, оператор ,(запятая) ограничен уровнем expression и не может содержать statement-конструкции:
int y = (if (x > 0) 1; else 2, 42); // ОШИБКА - if не Expression, нельзя вставить внутрь ","
И напоследок, comma operator имеет самый низкий приоритет среди операторов C, и область его применения жёстко ограничена контекстами, где парсер может быть точно уверен, что это не разделитель списка:
f(a, b, c); // это вызов с тремя аргументами, а не comma-expression!
f((a, b), c); // а вот это уже - comma-expression как первый аргумент
Часть 4. Формальная грамматика универсального синтаксисаЕсли попробовать описать expression и statement в общем виде, получится красивая взаимная рекурсия между ними:
Program := Statement
Expression := atomic-expr
| Expression binary-op Expression
| unary-op Expression
| Expression '(' arg-list ')' // вызов функции
| '{' Statement '}' // сгруппированный Statement - тоже Expression!
Statement := Expression
| Expression ';' Statement // рекурсивная композиция через ;
arg-list := Expression (',' Expression)*
Где:
Statement - это либо одно выражение, либо выражение, за которым следует ; и новый statement (рекурсивно).
Expression - это атомарное выражение (идентификатор, литерал, операция, вызов) либо { statement }, то есть блок, внутри которого находится statement, но который сам рассматривается как единое выражение.
Именно взаимная рекурсия, при которой блоки могут содержать statement, а statement строится из выражений, придаёт такой модели логичность и стройность, тогда как разные языки её по-разному ограничивают:
C-подобные языки разрешают переход { statement } -> expression только для простых выражений, но запрещают для управляющих конструкций. Поэтому if (x) { ... } нельзя использовать как выражение, хотя блок в фигурных скобках сам по себе мог бы быть выражением, если бы грамматика это допускала. Более того, GCC-расширение ({ ... }) буквально реализует такое правило: { statement } интерпретируется как expression, тогда как стандартный C так не умеет.
Lisp-подобные языки и Ruby вообще не нуждаются в отдельном нетерминале statement - в этих языках всё является выражением.
Rust оказался где-то посередине. В нём есть категория объявлений (let, fn, mod), которые являются инструкциями и не могут быть частью выражения. Тем не менее все управляющие конструкции (if, match, loop и т. д.) являются выражениями.
Разделение на expression и statement, привычное для многих поколений разработчиков C-подобных языков - не логическая необходимость, а историческая случайность, закреплённая архитектурой машин фон Неймана, которую перенесли из FORTRAN в ALGOL, а затем в C. Оно зафиксировалось в грамматиках как «удобное» решение на заре компиляторостроения, но не имеет фундаментальных причин на уровне синтаксиса.
Анализ развития синтаксиса разных языков программирования показывает, что statement можно рассматривать как структурную композицию выражений, а блоки в скобках - как инструмент обратной «упаковки» такой последовательности в единое целое.
Понимание истинной природы statement избавляет от ненужных условностей и даёт единый взгляд на синтаксис, в котором точка с запятой - всего лишь оператор, а фигурные скобки - не отличительный признак statement, а мост между двумя мирами, которые на самом деле, по сути, одно и тоже.
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | [Перевод] Понятие о конечных автоматах: руководство разработчика по предсказуемой логике приложений | 0 | 9.68 | 29-05-2026 |
| 2 | [Перевод] Возвращение аспектно-ориентированного программирования | 0 | 8.4 | 03-07-2026 |
| 3 | Волшебство естественного языка и практическое применение | 0 | 6.87 | 30-05-2026 |
| 4 | Мне надоело писать один и тот же код. Поэтому я сделал Featuregen | 3 | 6 | 09-07-2026 |
| 5 | Через меня проходит 5 тысяч чертежей в год, и 99% ошибок в них одинаковые | 0 | 9.5 | 23-07-2026 |
| 6 | Модель адаптивного стриминга или — как читать уравнения в программирование 2026, скриншоты | 0 | 14.1 | 19-07-2026 |
| 7 | [Перевод] Как на самом деле работают LLM | 0 | 7 | 07-07-2026 |
| 8 | std::expected в C++23: гайд по миграции с исключений на функциональный error handling | 0 | 7 | 07-07-2026 |
| 9 | JOIN как в ORM: связи по foreign key в PostgreSQL | 0 | 7.3 | 02-08-2026 |
| 10 | Основы вёрстки для дизайнера. Часть 1. Введение: как дизайн превращается в живой сайт | 5 | 7 | 07-07-2026 |