Я занимаюсь изучением синтаксиса языков программирования и пытаюсь отделить фундаментальные закономерности от исторических случайностей. Одна из таких случайностей — почти повсеместное использование терминов выражение (expression) и инструкция (statement) при описании и классификации синтаксиса языков программирования.
Интуитивно кажется, что 2 + 2 и if (x) { foo(); } — это совершенно разные сущности, но при более глубоком анализе оказывается, что подобное разделение искусственное и возникло из-за архитектурных особенностей вычислительных машин почти полвека назад и с тех пор просто «переходит» из языка в язык.
Часть 1. Историческая
Чтобы понять, откуда взялось разделение синтаксиса на выражения (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процессора — это действие, а не способ вычислить значение.
Lisp и параллельная вселенная без statement’ов
Почти синхронно с FORTRAN, в 1958 году, Джон Маккарти создал Lisp, фундаментально основанный на лямбда-исчислении. Здесь нет понятия statement — вся программа состоит из S-выражений: будь то вызов функции, условная конструкция или блок.
В Лиспе конструкция (if (> a b) a b) — это просто выражение, значение которого можно присвоить, передать в функцию или использовать в более сложной композиции. А блок (let ((x 5)) (+ x 1)) возвращает 6, оставаясь обычным выражением.
Это не «расширение» возможностей, а фундаментальный иной принцип: вся программа — это вычисляемые значения, а не последовательность управляющих инструкций. С этой позиции разделение на expression и statement выглядит ненужным.
Часть 2. Современные языки
Интересно посмотреть, насколько глубоко в современных языках укоренилась граница между этими терминами и как она преодолевается.
Классическая модель: 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: эмуляция выражения через функцию
JavaScript формально сохраняет C-подобное разделение, однако в язык встроены обходные инструменты. Используя IIFE (немедленно вызываемые функциональные выражения), разработчик может «обернуть» statement-логику в контекст, где требуется значение:
let y = (function() { if (x > 0) return 1; else return 2;})();
До появления современных JIT-оптимизаций подобный трюк нёс реальные накладные расходы на вызов функции, но он наглядно демонстрирует ручную эмуляцию expression-грамматики поверх statement. Показательно, что раз сообщество идёт на подобные ухищрения — значит, потребность в таком подходе велика.
Ruby: всё есть выражение
В Ruby любая конструкция — выражение, без исключений. Можно написать:
y = if x > 0 1else 2endz = case value when 1 then "one" when 2 then "two" else "other" endw = 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 := StatementExpression := 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, а мост между двумя мирами, которых на самом деле никогда не было.
ссылка на оригинал статьи https://habr.com/ru/articles/1065722/