Обновить

Если вы думаете, что знаете, чем отличается «Expression» от «Statement», то, скорее всего, вы ошибаетесь

Уровень сложностиСредний
Время на прочтение8 мин
Охват и читатели13K
Всего голосов 12: ↑10 и ↓2+12
Комментарии34

Комментарии 34

Вместо тысячи слов хотелось бы увидеть пример дизайна языка, где нет разделения на statements и expressions, но и нет уродливого побочного синтаксиса. Скажем, решаем мы, что последнее выражение это значение блока while, но тут мы пишем последней строкой break. И приходится либо задавать break возвращаемый тип, либо... у нас снова разделение.

Ruby ?

То есть, вы просто посмотрели синтаксис языка и решили, что в компиляторе нет разделения на expr/stmt?

Что значит “посмотрел синтаксис”? Это фундаментальная особенность языка.

В Ruby есть явное разделения на statement и expression, и в официальной документации подписано что есть что. Например: break, next и redo это statements.

А вот грамматические правила для statements в Ruby, это очень старая версия, от 2004 года, но тем не менее. Видно что есть stmt, а есть expr и expr_value.

stmt        : kALIAS fitem  fitem
            | kALIAS tGVAR tGVAR
            | kALIAS tGVAR tBACK_REF
            | kALIAS tGVAR tNTH_REF
            | kUNDEF undef_list
            | stmt kIF_MOD expr_value
            | stmt kUNLESS_MOD expr_value
            | stmt kWHILE_MOD expr_value
            | stmt kUNTIL_MOD expr_value
            | stmt kRESCUE_MOD stmt
            | klBEGIN ‘{’ compstmt ‘}’
            | klEND ‘{’ compstmt ‘}’
            | lhs ‘=’ command_call
            | mlhs ‘=’ command_call
            | var_lhs tOP_ASGN command_call
            | primary_value ‘[’ aref_args ‘]’ tOP_ASGN command_call
            | primary_value ‘.’ tIDENTIFIER tOP_ASGN command_call
            | primary_value ‘.’ tCONSTANT tOP_ASGN command_call
            | primary_value tCOLON2 tIDENTIFIER tOP_ASGN command_call
            | backref tOP_ASGN command_call
            | lhs '=' mrhs_basic
            | mlhs '=' mrhs
            | expr

Что вы имеете ввиду под “statements” ? Нет возвращает значение, отдельная категория грамматики, которую нельзя использовать как отдельное выражение или что-то еще?

Ведь “break, next и redo” - это часть грамматики языка, но она не является автономными лексическими единицами, чтобы их можно было использовать как отдельное выражение. Поэтому, да это действительно операторы, но их нельзя использовать по отдельности вне определенного контекста, т.е. они сами по себе не являются “неделимой лексической единицей” и сами по себе не являются выражениями.

Тогда как лексические конструкции, где эти термины могут быть использованы, являются полноценными выражениями, а тот же самый break способен возвращать значение.

Что вы имеете ввиду под “statements” ?

Имею ввиду общеиспользуемое их определение, которое в том числе применяется в официальной документации Ruby.

но их нельзя использовать по отдельности вне определенного контекста

Как и все другие в языке кроме корневого, с которого начинается парсинг.

А вот еще такой пример из документации:

However, when used as a modifier, if, else, while, until and rescue are statements but not expressions.

Statements that are not expressions cannot be used in contexts where an expression is expected, such as method arguments.

puts( 1 if true ) #=> SyntaxError

Имею ввиду общеиспользуемое их определение …

Общепринятое определение, это “statement” не возвращает значения, но в Ruby это не так. Так какое определение вы имеете ввиду?

Общепринятое определение, это “statement” не возвращает значения

Источник можно? В англоязычной википедии например используется "statement is a syntactic unit of an imperative programming language that expresses some action to be carried out", в русскоязычной "наименьшая автономная часть языка программирования ... Программа обычно представляет собой последовательность инструкций". И примерно то же самое написано во всех учебниках.

При этом каждое expression в Ruby буквально включено в statement. Это отражено как в его правилах грамматики: stmt : expr, так и в документации, что можно заметить по цитате выше: "statements that are not expressions ...". И Ruby в этом не одинок, в тех же C и C++ это тоже верно.

Тогда по вашему выходит, что “expression” не может содержать инструкции? А если может (а оно может), то в чем тогда между ними различие?

Тогда по вашему выходит, что “expression” не может содержать инструкции?

Напрямую не может, конечно. И это не "по моему", это прямо отражено в документациях, грамматиках, и реализациях любого языка программирования в котором заданы эти сущности. Нет, ну можно конечно заморочиться и создать собственный язык, где все это принципиально схлопнуто в одну сущность, но это будет не более чем академический (если не эзотерический) эксперимент.

в чем тогда между ними различие?

Отличие в том, что из последовательности statement состоит вся программа, а expression это лишь одна из подкатегорий statement в некоторых языках, кроме которой существуют множество других.

… в котором заданы эти сущности.

Это детали реализации грамматики, которые разработчики реализовали именно таким образом. В одном случае это будут одни правила, а в другом - другие. Просто разработчики написали их именно так и нетерминал в парсере назвали stmt, а в другом языке нетерминал грамматики взяли и назвали stmt_or_expr, но что в итоге доказывает или объясняет?

Все это только детали реализации, а не фундаментальная причина их использования, ведь в противном случае у нас просто не было бы причины для спора :-)

Отличие в том, что из последовательности statement состоит вся программа, а expression это лишь одна из подкатегорий statement в некоторых языках, кроме которой существуют множество других.

LISP, это не академический язык, в котором все является выражением и вся программа состоит тоже только из выражений.

Это детали реализации грамматики, которые разработчики реализовали именно таким образом

Так про что угодно можно сказать. Все ЯП это формальные системы, в которых если захотеть можно букве имени класса номер строки в исходниках присвоить. Но это не значит что можно перечеркнуть все устоявшиеся определения.

В одном случае это будут одни правила, а в другом - другие. Просто разработчики написали их именно так и нетерминал в парсере назвали stmt, а в другом языке нетерминал грамматики взяли и назвали stmt_or_expr, но что в итоге доказывает или объясняет?

Так уж случилось, что есть сотни императивных языков программирования, в которых отдельно stmt, отдельно expr, и назвать их можно как уходно, хоть a и b, но это две отдельные сущности, которые соотносятся определенным образом, и это не только деталь реализации, но и прописанная в документациях особенность. И так уж случилось, что нет языков, в которых есть один терминал названный stmt_or_expr, либо это две отдельные сущности, либо одной из них просто нет. Определения нужны, чтобы люди могли одинаково понимать о чем идет речь, когда слышат конкретное слово, и обычно они отражают конкретную сложившуюся по факту действительность. И она сложилась именно вот так.

Тем более, что речь изначально шла про Ruby, в котором это разделение такое же явное, как и в С/С++/Java. Т.е. прямо указано, что некоторые statement не являются expression, и использование их там, где ожидается expression является синтаксической ошибкой.

LISP, это не академический язык, в котором все является выражением и вся программа состоит тоже только из выражений.

LISP это не императивный язык, в нем программа это список атомов, а не последовательность строк/команд. В нем просто нет statement как сущности, и это не то же самое что тождественность stmt и expr. При этом он вполне академический.

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

Напишите пожалуйся “устоявшееся” определения, чем же, по вашему, «Expression» принципиально отличается от «Statement». Я нашел несколько определений, но все они имеют опровергающие контр примеры. Может быть у вас это получится лучше.

… Т.е. прямо указано, что некоторые statement не являются expression, и использование их там, где ожидается expression является синтаксической ошибкой.

Можете привести примеры этих statement?

LISP это не императивный язык, в нем программа это список атомов, а не последовательность строк. В нем просто нет statement как сущности, и это не то же самое что тождественность stmt и expr. При этом он вполне академический.

Давайте, пожалуйста, не будем притягивать за уши лишние сущности, и сперва определимся, чем же «Expression» принципиально отличается от «Statement», а потом уже посмотрим, что есть в LISP, а чего нет.

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

Напишите пожалуйся “устоявшееся” определения, чем же, по вашему, «Expression» принципиально отличается от «Statement»

Ну например.
Для statement: "syntactic unit of an imperative programming language that expresses some action to be carried out".
Для expression: "a sequence of operators and operands, that specifies a computation".

Можете привести примеры этих statement?

stmt if expr
stmt unless expr
stmt while expr
stmt until expr
stmt rescue expr
var = method expr
x,y = expr

Давайте, пожалуйста, не будем притягивать за уши лишние сущности, и сперва определимся, чем же «Expression» принципиально отличается от «Statement», а потом уже посмотрим, что есть в LISP, а чего нет.

Какие конкретно сущности являются лишними? Статья была посвящена терминам statement и expression, разделение которых продиктовано синтаксическими нюансами императивных ЯП, например банальным нежеланием разрешать и как-то обрабатывать конструкции вроде "print(return)", "if(break)", "while(import)" и т.п. Теперь вы приводите в пример LISP, в грамматике которого вообще не прописано ни statement, ни expression в значениях аналогичных императивным языкам.

Ведь я и пишу о том, что жесткое разделение на stmt и expr, это только дань привычке

Настолько же дань привычке, как использование латиницы в ключевых словах, организация кода по строкам с выполнением сверху вниз, позиционной десятиричной системы счисления. Можно ли сделать язык с египетскими иероглифами, выполнением по спирали на сетке против часовой стрелки и римскими числами? Можно. Является ли "дань привычке" основной причиной почему так не делают? Сомнительно.

Вот если сначала доказать, что альтернативный способ объективно лучше используемого (например, приводит к меньшему числу ошибок, или ускорению написание кода при прочих равных), но не используется без видимых причин - вот тогда это действительно может быть данью привычки.

В каждом языке свое определение разделения на stmt и expr итп

И, соответственно, записано в БНФ

Осталось взять и проанализировать

Осталось взять и проанализировать

Именно это я и сделал. Результаты анализа представлены в статье :-)

Я тогда не уловил смысл статьи.

Заголовок всегда true, поскольку разделение [на stmt, expr, op, ..] зависит от контекста (ЯП и даже местоположения лексической конструкции).

Популяризация чтения учебников и БНФ, рассказ как оно устроено и почему?

Для меня это выглядит как "небо синее". И далее либо ты это принимаешь как есть, либо лезешь в физику дифракции света.

Все так, разделение на stmt и expr зависит от языка, который, как правило, тянет за собой исторический груз, в том числе и из базовых учебников. Сперва мне самому захотелось разобраться в этом вопросе, а потом решил результатами поделиться с остальными.

Nemerle

Точно!

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

Для if(x>0) y; else z; возвращаемый тип это тип-сумма typeof(y) + typeof(z)

То есть если y это string а z это float то на выходе получаем "вариант" (дискриминированное объединение) с двумя полями string и float.

Если у нас цикл с предусловием while(x>0) y; или условие без альтернативной ветки if(x>0) y; то очевидно что результат - тип-сумма typeof(y) + never, что эквивалентно опционалу optional(typeof(y))

Разумеется эти типы должны быть структурными (не номинативными), чтобы была совместимость с любым явно определенным пользовательским номинативным типом.

При такой системе типов стандартные структурные стейтменты всегда будут совместимы с выражениями. Операторы "запятая" и "условие" можно будет убирать из языка чтобы они не создавали путаницы (по сути запятая это ведь разделитель элементов в списке, к примеру в списке аргументов функции или в литеральном кортеже, но если она еще и оператор - возникает ненужная путаница и усложнение).

Разумеется эти типы должны быть структурными (не номинативными), чтобы была совместимость с любым явно определенным пользовательским номинативным типом.

Подобное ограничение не обязательно (точнее, оно определяется системой типов языка).

Это не ограничение, а скорее удобство. Компилятор генерирует "нетерминальный" составной тип, и логично чтобы он вел себя так же, как литерал вида {1,2,3} в обычном Си, которым можно инициализировать объект любой структуры из трех числовых полей (увы, в Си эта тема неразвита, и просто присвоить или передать в функцию литерал не получится - только инициализировать, но как пример подойдет и инициализация).

Так я и говорю, чтоб подобное удобство зависит от системы типов языка. Если он статически типизируемый как С/С++ , тогда ему по любому нужен какой-то тип, чтобы было что проверять при компиляции. А, например, для динамически типизируемых языков конкретный тип выводить не обязательно. Для них без анализа будет достаточно какого нибудь Any, который все равно будет проверяться в рантайме.

Argentum

Будут ли новые материалы по Аргентуму ?

В языке, созданном для IBM 704, выражения (X + Y*Z) были строго отделены от управляющих инструкций (IF, DO, GOTO). Первые вычисляли значения, вторые управляли потоком исполнения и смешивать их между сбой было нельзя. Эта концепция синтаксиса напрямую отражала архитектуру вычислительных машин того времени: программа понималась как последовательность машинных команд, каждая из которых либо вычисляла операнд, либо изменяла счётчик команд, но не одновременно.

Вообще-то, современные вычислительные машины ничем в этом плане не отличаются: есть машинные команды, выполняющие вычисления, а есть выполняющие переходы (с анализом некоего условия или без такового). Иногда встречаются команды, выполняющие простейшее вычисление и условный переход в зависимости от результата -- главным образом, для организации циклов по счётчику, но такие есть не везде, да и не позволяют они делать "серьёзные" вычисления.

Это был прямой предшественник тернарного оператора

Во-первых, это, по свой сути, оно и есть, а не "предшественник". А во-вторых, в русском языке для действий в выражениях принято слово "операция" (арифметические операции и т.п. -- не операторы; в английском -- да, operator).

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

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

Ну а во-вторых, у PDP-11 нет команды JZ, хотя идея, высказанная автором, понятна.

архитектурой машин фон Неймана

Архитектура фон Неймана, как и гарвардская архитектура -- это не про то, какие команды есть и что они делают, а про то, используются ли для памяти единые адреса независимо от того, команды это или данные (фон Нейман), или же адресные пространства кода и данных строго отделены друг от друга и не пересекаются, а численно один и тот же адрес указывает разные ячейки памяти в зависимости от того, является ли он адресом команды или данных (Гарвард).

В общем, как по мне, слов в публикации много, а толку -- не очень...

Спасибо, вы поняли суть, но упустили из виду важный момент. Я старался делать акцент не на абстрактных командах микропроцессора (ассемблера), а на синтаксисе языка, как отображения этих команд.

То есть, если раньше синтаксис ЯВУ старался повторять идеологию машинных инструкций и архитектуру вычислителей, то сейчас (точнее уже много лет) синтаксис языков вообще никак не привязан к вычислительной архитектуре.

int y = foo(), bar(); // Это НЕ comma-expression! Это два отдельных объявления переменных!

Здесь объявление функции bar(), а не второй переменной.

Странно, что в статье не упоминается Kotlin, в котором почти все является expression, результат которого можно использовать. И есть типы Unit и Nothing :

fun executeAction(action: () -> Unit) {
    action() // Executes the lambda expression
}

fun main() {
    executeAction { println("Button clicked!") }
}
fun throwError(message: String): Nothing {
    throw IllegalArgumentException(message)
}

fun parseAge(input: String): Int {
    val age = input.toIntOrNull()
    
    // Valid because Nothing is a subtype of Int
    return age ?: throwError("Invalid age provided!") 
}
fun calculateComplexTax(income: Double): Double {
    // Compiles cleanly even though it doesn't return a Double yet!
    TODO("Implement regional tax calculation logic") 
}

На самом деле у меня изначально была еще и Java, но потом я подумал, что и так очень много примеров получается, поэтому Java (и Kotlin, как её творческую переработку) я в качестве примеров приводить уже не стал, хотя может быть именно Kotlin и стоило бы упомянуть.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации