Perché la maggior parte delle lingue mainstream non supporta la sintassi "x y z" per i confronti booleani a 3 vie?

33

Se voglio confrontare due numeri (o altre entità ben ordinate), lo farei con x < y . Se voglio confrontare tre di loro, lo studente di algebra delle scuole superiori suggerirà di provare x < y < z . Il programmatore in me risponderà quindi con "no, non è valido, devi fare x < y && y < z ".

La maggior parte delle lingue che ho incontrato non sembrano supportare questa sintassi, il che è strano dato quanto è comune in matematica. Python è un'eccezione notevole. JavaScript sembra come un'eccezione, ma in realtà è solo uno sfortunato sottoprodotto della precedenza degli operatori e delle conversioni implicite; in node.js, 1 < 3 < 2 valuta true , perché è in realtà (1 < 3) < 2 === true < 2 === 1 < 2 .

Quindi la mia domanda è questa: Perché x < y < z non è comunemente disponibile nei linguaggi di programmazione, con la semantica attesa?

    
posta JesseTG 27.04.2016 - 22:33
fonte

7 risposte

30

Questi sono operatori binari, che quando vengono concatenati generano normalmente e naturalmente un albero di sintassi astratto come:

Quandovienevalutato(chesifadallefoglieinsu),questoproduceunrisultatobooleanodax<y,quindisiottieneunerroreditipocheprovaafareboolean<z.Affinchéx<y<zfunzionicomehaidiscusso,devicreareuncasospecialenelcompilatoreperprodurreunalberodisintassicome:

Non è possibile farlo. Ovviamente lo è, ma aggiunge un po 'di complessità al parser per un caso che non si presenta davvero così spesso. In pratica stai creando un simbolo che a volte agisce come un operatore binario e talvolta agisce efficacemente come un operatore ternario, con tutte le implicazioni della gestione degli errori e di ciò che comporta. Ciò aggiunge un sacco di spazio affinché le cose vadano male che i progettisti di linguaggio preferirebbero evitare se possibile.

    
risposta data 28.04.2016 - 00:43
fonte
37

Perché x < y < z non è comunemente disponibile nei linguaggi di programmazione?

In questa risposta concludo che

  • anche se questo costrutto è banale da implementare nella grammatica di una lingua e crea valore per gli utenti della lingua,
  • i motivi principali per cui questo non esiste nella maggior parte delle lingue è dovuto alla sua importanza rispetto ad altre caratteristiche e alla riluttanza degli organi di governo delle lingue a
    • sconvolge gli utenti con modifiche potenzialmente violente
    • per spostarsi per implementare la funzione (es .: pigrizia).

Introduzione

Posso parlare dalla prospettiva di un pitista su questa domanda. Sono un utente di una lingua con questa funzione e mi piace studiare i dettagli di implementazione della lingua. Oltre a questo, ho una certa familiarità con il processo di modifica di linguaggi come C e C ++ (lo standard ISO è governato da comitato e versione per anno.) E ho visto sia Ruby che Python implementare i cambiamenti di rottura.

Documentazione e implementazione di Python

Dalla documentazione / grammatica, vediamo che possiamo concatenare qualsiasi numero di espressioni con operatori di confronto:

comparison    ::=  or_expr ( comp_operator or_expr )*
comp_operator ::=  "<" | ">" | "==" | ">=" | "<=" | "!="
                   | "is" ["not"] | ["not"] "in"

e la documentazione afferma inoltre:

Comparisons can be chained arbitrarily, e.g., x < y <= z is equivalent to x < y and y <= z, except that y is evaluated only once (but in both cases z is not evaluated at all when x < y is found to be false).

Equivalenza logica

Quindi

result = (x < y <= z)

è logicamente equivalente in termini di valutazione di x , y e z , con l'eccezione che y viene valutata due volte:

x_lessthan_y = (x < y)
if x_lessthan_y:       # z is evaluated contingent on x < y being True
    y_lessthan_z = (y <= z)
    result = y_lessthan_z
else:
    result = x_lessthan_y

Ancora una volta, la differenza è che y viene valutato solo una volta con (x < y <= z) .

(Nota, le parentesi sono completamente inutili e ridondanti, ma le ho usate a beneficio di quelle provenienti da altre lingue, e il codice sopra è un Python piuttosto legale.)

Ispezione dell'albero sintattico astratto analizzato

Possiamo esaminare come Python analizza gli operatori di confronto concatenati:

>>> import ast
>>> node_obj = ast.parse('"foo" < "bar" <= "baz"')
>>> ast.dump(node_obj)
"Module(body=[Expr(value=Compare(left=Str(s='foo'), ops=[Lt(), LtE()],
 comparators=[Str(s='bar'), Str(s='baz')]))])"

Quindi possiamo vedere che questo non è difficile da analizzare per Python o qualsiasi altra lingua.

>>> ast.dump(node_obj, annotate_fields=False)
"Module([Expr(Compare(Str('foo'), [Lt(), LtE()], [Str('bar'), Str('baz')]))])"
>>> ast.dump(ast.parse("'foo' < 'bar' <= 'baz' >= 'quux'"), annotate_fields=False)
"Module([Expr(Compare(Str('foo'), [Lt(), LtE(), GtE()], [Str('bar'), Str('baz'), Str('quux')]))])"

E contrariamente alla risposta attualmente accettata, l'operazione ternaria è un'operazione di confronto generica, che prende la prima espressione, un iterabile di confronti specifici e un iterabile di nodi di espressione per valutare se necessario. Semplice.

Conclusione su Python

Personalmente ritengo che la semantica della gamma sia abbastanza elegante, e la maggior parte dei professionisti di Python che conosco incoraggerebbero l'uso della funzione, invece di considerarla dannosa - la semantica è chiaramente indicata nella documentazione ben nota (come notato sopra ).

Nota che il codice viene letto molto più di quanto non sia scritto. Le modifiche che migliorano la leggibilità del codice dovrebbero essere accolte, non scontate aumentando gli spettri generici di Paura, incertezza e dubbio .

Allora perché x < y < z non è comunemente disponibile nei linguaggi di programmazione?

Penso che ci sia una confluenza di ragioni che centrano l'importanza relativa della caratteristica e il relativo momento / inerzia del cambiamento consentito dai governatori delle lingue.

Domande simili possono essere poste su altre funzioni linguistiche più importanti

Perché l'ereditarietà multipla non è disponibile in Java o C #? Qui non c'è una buona risposta a nessuna domanda . Forse gli sviluppatori erano troppo pigri, come sostiene Bob Martin, e le ragioni fornite sono solo scuse. E l'ereditarietà multipla è un argomento piuttosto importante nell'informatica. È certamente più importante della concatenazione dell'operatore.

Esistono soluzioni alternative

Il concatenamento degli operatori di confronto è elegante, ma non altrettanto importante quanto l'ereditarietà multipla. E proprio come Java e C # hanno interfacce come soluzione alternativa, così come ogni linguaggio per confronti multipli: si concatenano semplicemente i confronti con boolean "e" s, che funziona abbastanza facilmente.

La maggior parte delle lingue sono governate da un comitato

La maggior parte delle lingue si evolvono in commissione (piuttosto che avere un Dictator Benevolente per la vita come Python ha). E ipotizzo che questo problema non abbia visto abbastanza supporto per farlo uscire dalle rispettive commissioni.

Le lingue che non offrono questa funzione possono cambiare?

Se una lingua consente x < y < z senza la semantica matematica prevista, questo sarebbe un cambio di rottura. Se non lo permettesse in primo luogo, sarebbe quasi banale da aggiungere.

Ultime modifiche

Per quanto riguarda le lingue con cambiamenti radicali: aggiorniamo le lingue con i cambiamenti del comportamento di rottura - ma gli utenti tendono a non gradire questo, specialmente gli utenti di funzionalità che potrebbero essere violate. Se un utente fa affidamento sul precedente comportamento di x < y < z , probabilmente protesterebbe a voce alta. E poiché la maggior parte delle lingue sono governate da un comitato, dubito che avremmo molta volontà politica per sostenere un tale cambiamento.

    
risposta data 28.04.2016 - 00:42
fonte
13

I linguaggi del computer cercano di definire le unità più piccole possibili e consentono di combinarle. L'unità più piccola possibile è qualcosa come "x < y" che dà un risultato booleano.

Potresti chiedere un operatore ternario. Un esempio potrebbe essere x < y < z. Ora quali combinazioni di operatori autorizziamo? Ovviamente x > y > z o x > = y > = z o x > y > = z o forse x == y == z dovrebbe essere consentito. Che dire di x < y > z? x! = y! = z? Cosa significa l'ultimo, x! = Y e y! = Z o che tutti e tre sono diversi?

Ora promozioni di argomenti: in C o C ++, gli argomenti sarebbero promossi a un tipo comune. Quindi cosa fa x < y < la media di x è doppia ma y e z sono lunghi a lungo int? Tutti e tre hanno promosso il doppio? O sei preso come doppio una volta e tanto a lungo int l'altra volta? Cosa succede se in C ++ uno o entrambi gli operatori sono sovraccarichi?

E per ultimo, permetti un numero qualsiasi di operandi? Come un < b > c < d > e < f > g?

Bene, tutto diventa molto complicato. Ora quello che non mi dispiacerebbe è x < y < z producendo un errore di sintassi. Perché l'utilità di esso è piccola rispetto al danno fatto ai principianti che non riescono a capire cosa x < y < z lo fa davvero.

    
risposta data 27.04.2016 - 23:13
fonte
10

In molti linguaggi di programmazione, x < y è un'espressione binaria che accetta due operandi e valuta un singolo risultato booleano. Pertanto, se concateni più espressioni, true < z e false < z vince ' Ho senso, e se quelle espressioni valutano con successo, è probabile che producano il risultato sbagliato.

È molto più facile pensare a x < y come una chiamata di funzione che prende due parametri e produce un singolo risultato booleano. In effetti, è così che molte lingue lo implementano sotto il cofano. È componibile, facilmente compilabile e funziona.

Lo scenario x < y < z è molto più complicato. Ora il compilatore, in effetti, deve modellare le funzioni tre : x < y , y < z e il risultato di quei due valori uniti insieme, il tutto nel contesto di un discutibile grammatica linguistica ambigua .

Perché lo hanno fatto nell'altro modo? Perché è una grammatica non ambigua, molto più facile da implementare e molto più facile da correggere.

    
risposta data 27.04.2016 - 22:46
fonte
6

La maggior parte delle lingue tradizionali sono (almeno in parte) orientate agli oggetti. Fondamentalmente, il principio di base di OO è che gli oggetti inviano messaggi ad altri oggetti (o loro stessi), e il ricevitore di quel messaggio ha il controllo completo su come rispondere a quel messaggio.

Ora vediamo come implementeremo qualcosa di simile

a < b < c

Potremmo valutarlo rigorosamente da sinistra a destra (associativo a sinistra):

a.__lt__(b).__lt__(c)

Ma ora chiamiamo __lt__ sul risultato di a.__lt__(b) , che è un Boolean . Non ha senso.

Prova a associare a destra:

a.__lt__(b.__lt__(c))

Nah, anche questo non ha senso. Ora abbiamo a < (something that's a Boolean) .

Ok, che ne dici trattandolo come zucchero sintattico. Facciamo una catena di n < confronti invia un messaggio n-1-ary. Questo potrebbe significare che inviamo il messaggio __lt__ a a , passando b e c come argomenti:

a.__lt__(b, c)

Ok, funziona, ma qui c'è una strana asimmetria: a arriva a decidere se è inferiore a b . Ma b non arriva a decidere se è inferiore a c , invece quella decisione è anche fatta da a .

Che ne dici di interpretarlo come un messaggio n-ario inviato a this ?

this.__lt__(a, b, c)

Finalmente! Questo può funzionare. Significa, tuttavia, che l'ordinamento degli oggetti non è più una proprietà dell'oggetto (ad esempio se a è minore di b non è né una proprietà di a né di b ) ma invece una proprietà del contesto (ovvero this ).

Da un punto di vista tradizionale che sembra strano. Tuttavia, ad es. in Haskell, è normale. Potrebbero esserci più implementazioni diverse della classe di tipi Ord , ad esempio, e se a è inferiore a b , dipende da quale istanza del classificatore si trova nello scope.

Ma in realtà non è quello strano a tutti! Sia Java ( Comparator ) e .NET ( IComparer ) hanno interfacce che consentono di inserire la tua relazione di ordinamento in es. algoritmi di ordinamento. Quindi, riconoscono pienamente che un ordinamento non è qualcosa che è fissato a un tipo, ma dipende dal contesto.

Per quanto ne so, al momento non ci sono lingue che eseguono una tale traduzione. Esiste tuttavia una precedenza: entrambi Ioke e Seph avere ciò che i loro progettisti chiamano "operatori trinari" - operatori che sono sintatticamente binari, ma semanticamente ternari. In particolare,

a = b

è non interpretato come invio del messaggio = a a che passa b come argomento, ma piuttosto come invio del messaggio = al "terreno corrente" (un concetto simile ma non identico a this ) passando a e b come argomenti. Quindi, a = b è interpretato come

=(a, b)

e non

a =(b)

Questo potrebbe facilmente essere generalizzato agli operatori n-ari.

Si noti che questo è davvero peculiare delle lingue OO. In OO, abbiamo sempre un singolo oggetto che è in definitiva responsabile dell'interpretazione di un messaggio inviato e, come abbiamo visto, non è immediatamente ovvio per qualcosa come a < b < c quale oggetto dovrebbe essere.

Questo però non si applica ai linguaggi procedurali o funzionali. Ad esempio, in Schema , Common Lisp e Clojure , la funzione < è n-ary e può essere chiamata con un numero arbitrario di argomenti.

In particolare, < non significa "minore di", piuttosto queste funzioni sono interpretate in modo leggermente diverso:

(<  a b c d) ; the sequence a, b, c, d is monotonically increasing
(>  a b c d) ; the sequence a, b, c, d is monotonically decreasing
(<= a b c d) ; the sequence a, b, c, d is monotonically non-decreasing
(>= a b c d) ; the sequence a, b, c, d is monotonically non-increasing
    
risposta data 28.04.2016 - 01:18
fonte
3

È semplicemente perché i progettisti di linguaggi non ci hanno pensato o non hanno pensato che fosse una buona idea. Python lo fa come hai descritto con una semplice (quasi) grammatica LL (1).

    
risposta data 27.04.2016 - 23:00
fonte
2

Il seguente programma C ++ compila con nary un peep da clang, anche con gli avvisi impostati sul livello più alto possibile ( -Weverything ):

#include <iostream>
int main () { std::cout << (1 < 3 < 2) << '\n'; }

La suite del compilatore gnu d'altra parte mi avvisa che comparisons like 'X<=Y<=Z' do not have their mathematical meaning [-Wparentheses] .

So, my question is this: why is x < y < z not commonly available in programming languages, with the expected semantics?

La risposta è semplice: compatibilità con le versioni precedenti. Esiste una grande quantità di codice in the wild che utilizza l'equivalente di 1<3<2 e si aspetta che il risultato sia true-ish.

Un progettista di linguaggi ha una sola possibilità di ottenere questo "giusto", e questo è il momento in cui il linguaggio viene inizialmente progettato. Prenderlo "sbagliato" significa inizialmente che altri programmatori trarranno rapidamente vantaggio da quel comportamento "sbagliato". Ottenendolo "correttamente" la seconda volta interromperà il codice esistente.

    
risposta data 28.04.2016 - 12:51
fonte