Analisadores normais, como geralmente são ensinados, têm um estágio de lexer antes que o analisador toque na entrada. O lexer (também "scanner" ou "tokenizer") divide a entrada em pequenos tokens anotados com um tipo. Isso permite que o analisador principal use tokens como elementos terminais em vez de precisar tratar cada caractere como um terminal, o que leva a ganhos de eficiência perceptíveis. Em particular, o lexer também pode remover todos os comentários e espaços em branco. No entanto, uma fase separada do tokenizer significa que as palavras-chave também não podem ser usadas como identificadores (a menos que o idioma suporte stropping que tenha caído em desuso, ou prefixe todos os identificadores com um símbolo $foo).
Por quê? Vamos supor que temos um tokenizador simples que compreende os seguintes tokens:
FOR = 'for'
LPAREN = '('
RPAREN = ')'
IN = 'in'
IDENT = /\w+/
COLON = ':'
SEMICOLON = ';'
O tokenizer sempre corresponderá ao token mais longo e preferirá palavras-chave a identificadores. Assim interestingserá lexado como IDENT:interesting, mas inserá lexado como IN, nunca como IDENT:interesting. Um trecho de código como
for(var in expression)
será traduzido para o fluxo de token
FOR LPAREN IDENT:var IN IDENT:expression RPAREN
Até agora, isso funciona. Mas qualquer variável inseria lexada como a palavra-chave em INvez de uma variável, o que quebraria o código. O lexer não mantém nenhum estado entre os tokens e não pode saber que ingeralmente deve ser uma variável, exceto quando estamos em um loop for. Além disso, o código a seguir deve ser legal:
for(in in expression)
O primeiro inseria um identificador, o segundo seria uma palavra-chave.
Há duas reações a esse problema:
Palavras-chave contextuais são confusas, vamos reutilizar palavras-chave.
Java possui muitas palavras reservadas, algumas das quais não têm utilidade, exceto por fornecer mensagens de erro mais úteis aos programadores que mudam para C ++ em Java. A adição de novas palavras-chave quebra o código. A adição de palavras-chave contextuais é confusa para o leitor do código, a menos que ele tenha um bom destaque de sintaxe e dificulte a implementação de ferramentas, pois elas terão que usar técnicas de análise mais avançadas (veja abaixo).
Quando queremos estender o idioma, a única abordagem sensata é usar símbolos que antes não eram legais no idioma. Em particular, estes não podem ser identificadores. Com a sintaxe do loop foreach, o Java reutilizou a :palavra-chave existente com um novo significado. Com as lambdas, o Java adicionou uma ->palavra - chave que não poderia ocorrer anteriormente em nenhum programa jurídico ( -->ainda seria lexada como '--' '>'legal e ->pode ter sido lexada como '-', '>', mas essa sequência seria rejeitada pelo analisador).
Palavras-chave contextuais simplificam idiomas, vamos implementá-los
Lexers são indiscutivelmente úteis. Mas, em vez de executar um lexer antes do analisador, podemos executá-lo em conjunto com o analisador. Os analisadores de baixo para cima sempre sabem o conjunto de tipos de token que seriam aceitáveis em qualquer local. O analisador pode solicitar ao lexer que corresponda a qualquer um desses tipos na posição atual. Em um loop for-each, o analisador estaria na posição indicada ·na gramática (simplificada) depois que a variável fosse encontrada:
for_loop = for_loop_cstyle | for_each_loop
for_loop_cstyle = 'for' '(' declaration · ';' expression ';' expression ')'
for_each_loop = 'for' '(' declaration · 'in' expression ')'
Nessa posição, os tokens legais são SEMICOLONou IN, mas não IDENT. Uma palavra-chave inseria totalmente inequívoca.
Neste exemplo em particular, os analisadores de cima para baixo também não teriam problemas, pois podemos reescrever a gramática acima para
for_loop = 'for' '(' declaration · for_loop_rest ')'
for_loop_rest = · ';' expression ';' expression
for_loop_rest = · 'in' expression
e todos os tokens necessários para a decisão podem ser vistos sem retroceder.
Considere a usabilidade
O Java sempre tendeu à simplicidade semântica e sintática. Por exemplo, o idioma não suporta sobrecarga do operador, pois isso tornaria o código muito mais complicado. Portanto, ao decidir entre ine :para uma sintaxe de loop para cada um, precisamos considerar qual é menos confuso e mais aparente para os usuários. O caso extremo provavelmente seria
for (in in in in())
for (in in : in())
(Nota: Java possui espaços de nomes separados para nomes de tipos, variáveis e métodos. Acho que isso foi um erro, principalmente. Isso não significa que o design posterior da linguagem precise adicionar mais erros.)
Qual alternativa fornece separações visuais mais claras entre a variável de iteração e a coleção iterada? Qual alternativa pode ser reconhecida mais rapidamente quando você olha o código? Descobri que os símbolos de separação são melhores do que uma sequência de palavras quando se trata desses critérios. Outros idiomas têm valores diferentes. Por exemplo, o Python explica muitos operadores em inglês para que possam ser lidos naturalmente e fáceis de entender, mas essas mesmas propriedades podem dificultar bastante a compreensão de um pedaço do Python de relance.