I parser normali come vengono generalmente insegnati hanno uno stadio lexer prima che il parser tocchi l'input. Il lexer (anche "scanner" o "tokenizer") taglia l'input in piccoli token che sono annotati con un tipo. Ciò consente al parser principale di usare token come elementi terminali piuttosto che dover trattare ciascun carattere come un terminale, il che porta a guadagni di efficienza evidenti. In particolare, il lexer può anche rimuovere tutti i commenti e lo spazio bianco. Tuttavia, una fase separata di tokenizer significa che le parole chiave non possono essere utilizzate anche come identificatori (a meno che la lingua supporti stropping che è in qualche modo caduto in disgrazia o prefisso tutti gli identificatori con un sigillo come $foo
).
Perché? Supponiamo di avere un semplice tokenizzatore che comprende i seguenti token:
FOR = 'for'
LPAREN = '('
RPAREN = ')'
IN = 'in'
IDENT = /\w+/
COLON = ':'
SEMICOLON = ';'
Il tokenizer corrisponderà sempre al token più lungo e preferirà le parole chiave agli identificatori. Quindi interesting
sarà lexed come IDENT:interesting
, ma in
sarà lexed come IN
, mai come IDENT:interesting
. Un frammento di codice come
for(var in expression)
sarà tradotto nello stream di token
FOR LPAREN IDENT:var IN IDENT:expression RPAREN
Finora, funziona. Ma qualsiasi variabile in
verrebbe lessicata come la parola chiave IN
piuttosto che una variabile, che interromperà il codice. Il lexer non mantiene nessuno stato tra i token e non può sapere che in
dovrebbe essere normalmente una variabile tranne quando siamo in un ciclo for. Inoltre, il seguente codice dovrebbe essere legale:
for(in in expression)
Il primo in
sarebbe un identificatore, il secondo sarebbe una parola chiave.
Ci sono due reazioni a questo problema:
Le parole chiave contestuali sono confuse, riutilizziamo invece le parole chiave.
Java ha molte parole riservate, alcune delle quali non hanno utilità se non quella di fornire messaggi di errore più utili ai programmatori che passano a Java dal C ++. L'aggiunta di nuove parole chiave interrompe il codice. L'aggiunta di parole chiave contestuali è fonte di confusione per un lettore del codice, a meno che non abbiano una buona evidenziazione della sintassi e rende gli strumenti difficili da implementare perché dovranno utilizzare tecniche di analisi più avanzate (vedi sotto).
Quando vogliamo estendere la lingua, l'unico approccio sano è usare simboli che in precedenza non erano legali nella lingua. In particolare, questi non possono essere identificatori. Con la sintassi del ciclo foreach, Java ha riutilizzato la parola chiave :
esistente con un nuovo significato. Con lambdas, Java ha aggiunto una parola chiave ->
che in precedenza non poteva verificarsi in nessun programma legale ( -->
sarebbe ancora lexed come '--' '>'
che è legale e ->
potrebbe essere stato precedentemente lexato come '-', '>'
, ma quella sequenza verrebbe rifiutata dal parser).
Le parole chiave contestuali semplificano le lingue, implementiamole
I lessici sono indiscutibilmente utili. Ma invece di eseguire un lexer prima del parser, possiamo eseguirli in tandem con il parser. I parser bottom-up conoscono sempre l'insieme di tipi di token che sarebbero accettabili in qualsiasi posizione specifica. Il parser può quindi richiedere al lexer di trovare uno qualsiasi di questi tipi nella posizione corrente. In un ciclo for-each, il parser si troverà nella posizione indicata da ·
nella grammatica (semplificata) dopo che la variabile è stata trovata:
for_loop = for_loop_cstyle | for_each_loop
for_loop_cstyle = 'for' '(' declaration · ';' expression ';' expression ')'
for_each_loop = 'for' '(' declaration · 'in' expression ')'
In questa posizione, i token legali sono SEMICOLON
o IN
, ma non IDENT
. Una parola chiave in
sarebbe completamente non ambigua.
In questo particolare esempio, i parser top-down non avrebbero alcun problema dato che possiamo riscrivere la grammatica di cui sopra
for_loop = 'for' '(' declaration · for_loop_rest ')'
for_loop_rest = · ';' expression ';' expression
for_loop_rest = · 'in' expression
e tutti i token necessari per la decisione possono essere visualizzati senza retrocedere.
Considera l'usabilità
Java è sempre stato orientato verso la semplicità semantica e sintattica. Ad esempio, la lingua non supporta l'overloading dell'operatore perché renderebbe il codice molto più complicato. Pertanto, quando decidiamo tra in
e :
per una sintassi del ciclo for-each, dobbiamo considerare quale è meno confuso e più evidente per gli utenti. Il caso estremo sarebbe probabilmente
for (in in in in())
for (in in : in())
(Nota: Java ha spazi dei nomi separati per nomi di tipi, variabili e metodi.Penso che questo sia stato un errore, soprattutto.Questo non significa che la progettazione linguistica successiva debba aggiungere più errori.)
Quale alternativa offre separazioni visive più chiare tra la variabile di iterazione e la collezione iterata? Quale alternativa può essere riconosciuta più rapidamente quando si guarda il codice? Ho scoperto che separare i simboli è meglio di una stringa di parole quando si tratta di questi criteri. Altre lingue hanno valori diversi. Per esempio. Python spiega molti operatori in inglese in modo che possano essere letti in modo naturale e facili da capire, ma quelle stesse proprietà possono rendere difficile la comprensione di un pezzo di Python a colpo d'occhio.