Нормальные парсеры, как их обычно учат, имеют стадию лексера, прежде чем парсер коснется ввода. Лексер (также «сканер» или «токенизатор») разбивает входные данные на маленькие токены, помеченные типом. Это позволяет основному синтаксическому анализатору использовать токены в качестве терминальных элементов, а не обрабатывать каждый символ как терминальный, что приводит к заметному повышению эффективности. В частности, лексер также может удалить все комментарии и пробелы. Тем не менее, отдельная фаза токенизатора означает, что ключевые слова также не могут использоваться в качестве идентификаторов (если язык не поддерживает удаление, которое несколько утратило популярность, или префикс всех идентификаторов с символом, подобным символу $foo
).
Почему? Предположим, у нас есть простой токенизатор, который понимает следующие токены:
FOR = 'for'
LPAREN = '('
RPAREN = ')'
IN = 'in'
IDENT = /\w+/
COLON = ':'
SEMICOLON = ';'
Токенайзер всегда будет соответствовать самому длинному токену и предпочитать ключевые слова идентификаторам. Так interesting
будет лексировано как IDENT:interesting
, но in
будет лексировано как IN
никогда IDENT:interesting
. Фрагмент кода как
for(var in expression)
будет переведен в поток токенов
FOR LPAREN IDENT:var IN IDENT:expression RPAREN
Пока что это работает. Но любая переменная in
будет указана как ключевое слово, IN
а не как переменная, что нарушит код. Лексер не хранит никакого состояния между токенами и не может знать, что это in
обычно должна быть переменная, кроме случаев, когда мы находимся в цикле for. Также следующий код должен быть законным:
for(in in expression)
Первый in
будет идентификатором, второй будет ключевым словом.
Есть две реакции на эту проблему:
Контекстные ключевые слова сбивают с толку, давайте вместо этого будем использовать ключевые слова.
В Java много зарезервированных слов, некоторые из которых бесполезны, за исключением предоставления более полезных сообщений об ошибках программистам, переходящим на Java с C ++. Добавление новых ключевых слов нарушает код. Добавление контекстных ключевых слов вводит читателя в заблуждение, если у них нет хорошей подсветки синтаксиса, и затрудняет реализацию инструментов, поскольку им придется использовать более продвинутые методы синтаксического анализа (см. Ниже).
Когда мы хотим расширить язык, единственным разумным подходом является использование символов, которые ранее были недопустимы в языке. В частности, это не могут быть идентификаторы. С помощью синтаксиса цикла foreach Java повторно использовала существующее :
ключевое слово с новым значением. С лямбдами Java добавила ->
ключевое слово, которое ранее не могло встречаться ни в одной легальной программе ( -->
все равно будет помечено как '--' '>'
допустимое и ->
ранее могло быть помечено как '-', '>'
, но эта последовательность будет отклонена синтаксическим анализатором).
Контекстные ключевые слова упрощают языки, давайте их реализовывать
Лексические бесспорно полезны. Но вместо того, чтобы запускать лексер перед парсером, мы можем запускать их вместе с парсером. Восходящие синтаксические анализаторы всегда знают набор типов токенов, которые были бы приемлемы в любом заданном месте. Затем парсер может запросить лексер для сопоставления с любым из этих типов в текущей позиции. В цикле for-each парсер будет в позиции, обозначенной ·
в (упрощенной) грамматике после того, как переменная найдена:
for_loop = for_loop_cstyle | for_each_loop
for_loop_cstyle = 'for' '(' declaration · ';' expression ';' expression ')'
for_each_loop = 'for' '(' declaration · 'in' expression ')'
На этой позиции легальными токенами являются SEMICOLON
или IN
, но нет IDENT
. Ключевое слово in
было бы совершенно однозначным.
В этом конкретном примере парсеры сверху вниз также не будут иметь проблем, поскольку мы можем переписать приведенную выше грамматику в
for_loop = 'for' '(' declaration · for_loop_rest ')'
for_loop_rest = · ';' expression ';' expression
for_loop_rest = · 'in' expression
и все жетоны, необходимые для решения, можно увидеть без возврата.
Учитывайте удобство использования
Ява всегда была склонна к семантической и синтаксической простоте. Например, язык не поддерживает перегрузку операторов, потому что это сделает код намного более сложным. Таким образом, при выборе между in
и :
для синтаксиса цикла for-each, мы должны учитывать, что является менее запутанным и более очевидным для пользователей. Крайний случай, вероятно, будет
for (in in in in())
for (in in : in())
(Примечание: Java имеет отдельные пространства имен для имен типов, переменных и методов. Я думаю, что это, в основном, ошибка. Это не означает, что в более поздних версиях языка придется добавлять больше ошибок.)
Какая альтернатива обеспечивает более четкое визуальное разделение между переменной итерации и повторяющейся коллекцией? Какую альтернативу можно распознать быстрее, если взглянуть на код? Я обнаружил, что разделяющие символы лучше, чем цепочка слов, когда дело доходит до этих критериев. Другие языки имеют разные значения. Например, Python расшифровывает множество операторов на английском языке, чтобы их можно было читать естественным образом и легко понять, но эти же свойства могут затруднить понимание части Python с первого взгляда.