Qual è il vantaggio di far restituire un valore all'operatore di assegnazione?


27

Sto sviluppando un linguaggio che intendo sostituire sia Javascript che PHP. (Non riesco a vedere alcun problema con questo. Non è che nessuna di queste lingue abbia una grande base di installazione.)

Una delle cose che volevo cambiare era trasformare l'operatore di assegnazione in un comando di assegnazione, rimuovendo la possibilità di utilizzare il valore restituito.

x=1;          /* Assignment. */
if (x==1) {}  /* Comparison. */
x==1;         /* Error or warning, I've not decided yet. */
if (x=1) {}   /* Error. */

So che questo significherebbe che quelle funzioni a una riga che le persone C amano così tanto non funzionerebbero più. Ho pensato (con poche prove al di là della mia esperienza personale) che la stragrande maggioranza delle volte ciò è accaduto, era davvero inteso come un'operazione di confronto.

O è? Esistono usi pratici del valore di ritorno dell'operatore di assegnazione che non possono essere banalmente riscritti? (Per qualsiasi lingua che abbia un tale concetto.)


12
JS e PHP non hanno una grande "base di installazione"?
mhr,

47
@mri sospetto sarcasmo.
Andy Hunt,

12
L'unico caso utile che posso ricordare è while((x = getValue()) != null) {}. Le sostituzioni saranno più brutte poiché dovrai utilizzare breako ripetere il x = getValuecompito.
CodesInChaos,

12
@mri Ooh no, ho sentito che queste due lingue sono solo cose banali senza alcun investimento significativo. Una volta che le poche persone che insistono sull'uso di JS vedono la mia lingua, passeranno alla mia e non dovranno più scrivere ===. Sono altrettanto sicuro che i produttori di browser lanceranno immediatamente un aggiornamento che include la mia lingua insieme a JS. :)
billpg,

4
Ti suggerirei che se la tua intenzione è quella di migliorare una lingua esistente e intendi che sia adottata ampiamente, allora la compatibilità al 100% con la lingua esistente è buona. Vedi TypeScript come un esempio. Se la tua intenzione è quella di fornire una migliore alternativa a una lingua esistente, allora hai un problema molto più difficile. Una nuova lingua deve risolvere un problema realistico esistente molto meglio della lingua esistente per pagare il costo del passaggio. L'apprendimento di una lingua è un investimento e deve pagare.
Eric Lippert,

Risposte:


25

Tecnicamente, può valere la pena conservare un po 'di zucchero sintattico anche se può essere banalmente sostituito, se migliora la leggibilità di alcune operazioni comuni. Ma l'assegnazione come espressione non rientra in questo. Il pericolo di digitarlo al posto di un confronto significa che è usato raramente (a volte addirittura proibito dalle guide di stile) e provoca un doppio take ogni volta che viene utilizzato. In altre parole, i vantaggi della leggibilità sono piccoli in numero e grandezza.

Uno sguardo alle lingue esistenti che lo fanno può essere utile.

  • Java e C # mantengono un'espressione ma rimuovono la trappola menzionata richiedendo condizioni da valutare in booleane. Questo per lo più sembra funzionare bene, anche se a volte le persone si lamentano che questo non consente condizioni come if (x)al posto di if (x != null)o a if (x != 0)seconda del tipo di x.
  • Python rende l'assegnazione un'istruzione corretta anziché un'espressione. Le proposte per modificarlo occasionalmente raggiungono la mailing list di Python-Ideas, ma la mia impressione soggettiva è che ciò accada più raramente e generi meno rumore ogni volta rispetto ad altre funzionalità "mancanti" come loop do-while, switch switch, lambda multi-linea, eccetera.

Tuttavia, Python permette un caso speciale, assegnando a più nomi in una sola volta: a = b = c. Questa è considerata un'affermazione equivalente a b = c; a = b, ed è usata occasionalmente, quindi potrebbe valere la pena aggiungere anche alla tua lingua (ma non vorrei sudare, poiché questa aggiunta dovrebbe essere compatibile con le versioni precedenti).


5
+1 per il richiamo a a = b = ccui le altre risposte non portano realmente.
Leone,

6
Una terza risoluzione consiste nell'utilizzare un simbolo diverso per l'assegnazione. Pascal utilizza :=per il compito.
Brian,

5
@Brian: In effetti, così come C #. =è compito, ==è confronto.
Marjan Venema,

3
In C #, qualcosa come if (a = true)lancerà un avviso C4706 ( The test value in a conditional expression was the result of an assignment.). Anche GCC con C lancerà a warning: suggest parentheses around assignment used as truth value [-Wparentheses]. Questi avvertimenti possono essere messi a tacere con una serie aggiuntiva di parentesi, ma sono lì per incoraggiare esplicitamente che il compito era intenzionale.
Bob,

2
@delnan Solo un commento un po 'generica, ma è stata provocata dalla "rimuovere la trappola si parla richiedendo condizioni per valutare a booleani" - a = true non restituire un valore booleano e non è quindi un errore, ma solleva anche un avvertimento relativo in C #.
Bob,

11

Esistono usi pratici del valore di ritorno dell'operatore di assegnazione che non possono essere banalmente riscritti?

In generale, no. L'idea di avere il valore di un'espressione di assegnazione come valore assegnato significa che abbiamo un'espressione che può essere usata sia per il suo effetto collaterale che per il suo valore , e che molti considerano confusa.

Gli usi comuni sono in genere per rendere le espressioni compatte:

x = y = z;

ha la semantica in C # di "converti z nel tipo di y, assegna il valore convertito in y, il valore convertito è il valore dell'espressione, converti quello nel tipo di x, assegna in x".

Ma siamo già nel regno degli effetti collaterali impertivi in ​​un contesto di affermazione, quindi ci sono davvero pochi e convincenti benefici

y = z;
x = y;

Allo stesso modo con

M(x = 123);

essere una scorciatoia per

x = 123;
M(x);

Ancora una volta, nel codice originale stiamo usando un'espressione sia per i suoi effetti collaterali che per il suo valore, e stiamo facendo una dichiarazione che ha due effetti collaterali invece di uno. Entrambi sono puzzolenti; cerca di avere un effetto collaterale per istruzione e usa le espressioni per i loro valori, non per i loro effetti collaterali.

Sto sviluppando un linguaggio che intendo sostituire sia Javascript che PHP.

Se vuoi davvero essere audace e sottolineare che il compito è una dichiarazione e non un'uguaglianza, allora il mio consiglio è: rendilo chiaramente una dichiarazione di compito .

let x be 1;

Quello rosso. O

x <-- 1;

o anche meglio:

1 --> x;

O ancora meglio

1 → x;

Non c'è assolutamente modo di confonderne qualcuno x == 1.


1
Il mondo è pronto per i simboli Unicode non ASCII nei linguaggi di programmazione?
billpg,

Per quanto mi piacerebbe quello che suggerisci, uno dei miei obiettivi è che la maggior parte dei JavaScript "ben scritti" possano essere trasferiti con modifiche minime o nulle.
billpg,

2
@billpg: il mondo è pronto ? Non lo so - il mondo era pronto per APL nel 1964, decenni prima dell'invenzione di Unicode? Ecco un programma in APL che seleziona una permutazione casuale di sei numeri tra i primi 40: x[⍋x←6?40] APL richiedeva una propria tastiera speciale, ma era un linguaggio abbastanza riuscito.
Eric Lippert,

@billpg: Macintosh Programmer's Workshop ha usato simboli non ASCII per cose come i tag regex o il reindirizzamento di stderr. D'altra parte, MPW aveva il vantaggio che il Macintosh rendeva semplice la digitazione di caratteri non ASCII. Devo confessare un po 'di perplessità sul perché il driver della tastiera statunitense non fornisca alcun mezzo decente per digitare caratteri non ASCII. Non solo l'immissione del numero Alt richiede la ricerca di codici carattere, ma in molte applicazioni non funziona nemmeno.
supercat

Hm, perché si preferisce assegnare "a destra" come a+b*c --> x? Mi sembra strano.
Ruslan,

9

Molte lingue scelgono il percorso per rendere l'assegnazione un'istruzione anziché un'espressione, incluso Python:

foo = 42 # works
if foo = 42: print "hi" # dies
bar(foo = 42) # keyword arg

e Golang:

var foo int
foo = 42 # works
if foo = 42 { fmt.Printn("hi") } # dies

Altre lingue non hanno assegnazioni, ma piuttosto associazioni con ambito, ad esempio OCaml:

let foo = 42 in
  if foo = 42 then
    print_string "hi"

Tuttavia, letè un'espressione stessa.

Il vantaggio di consentire l'assegnazione è che possiamo verificare direttamente il valore di ritorno di una funzione all'interno del condizionale, ad esempio in questo frammento di Perl:

if (my $result = some_computation()) {
  say "We succeeded, and the result is $result";
}
else {
  warn "Failed with $result";
}

Perl ambisce inoltre alla dichiarazione solo a quella condizionale, il che la rende molto utile. Avvertirà anche se si assegna all'interno di un condizionale senza dichiarare una nuova variabile lì - if ($foo = $bar)avvertirà, if (my $foo = $bar)non lo farà.

Effettuare l'assegnazione in un'altra istruzione è generalmente sufficiente, ma può causare problemi di scoping:

my $result = some_computation()
if ($result) {
  say "We succeeded, and the result is $result";
}
else {
  warn "Failed with $result";
}
# $result is still visible here - eek!

Golang si basa fortemente sui valori di ritorno per il controllo degli errori. Consente quindi a un condizionale di prendere una dichiarazione di inizializzazione:

if result, err := some_computation(); err != nil {
  fmt.Printf("Failed with %d", result)
}
fmt.Printf("We succeeded, and the result is %d\n", result)

Altre lingue usano un sistema di tipi per non consentire espressioni non booleane all'interno di un condizionale:

int foo;
if (foo = bar()) // Java does not like this

Naturalmente ciò fallisce quando si utilizza una funzione che restituisce un valore booleano.

Ora abbiamo visto diversi meccanismi per difendersi da incarichi accidentali:

  • Non consentire l'assegnazione come espressione
  • Utilizzare il controllo del tipo statico
  • L'assegnazione non esiste, abbiamo solo letattacchi
  • Consenti una dichiarazione di inizializzazione, non consentire diversamente l'assegnazione
  • Non consentire l'assegnazione all'interno di un condizionale senza dichiarazione

Li ho classificati in ordine di preferenza crescente - i compiti all'interno delle espressioni possono essere utili (ed è semplice aggirare i problemi di Python avendo una sintassi di dichiarazione esplicita e una diversa sintassi dell'argomento denominato). Ma va bene impedirle, poiché ci sono molte altre opzioni per lo stesso effetto.

Il codice privo di bug è più importante del codice conciso.


+1 per "Non consentire l'assegnazione come espressione". I casi d'uso per assegnazione come espressione non giustificano il potenziale di bug e problemi di leggibilità.
colpì il

7

Hai detto "Ho pensato (con poche prove al di là della mia esperienza personale) che la stragrande maggioranza delle volte ciò è accaduto, era davvero inteso come un'operazione di confronto".

Perché non FIX THE PROBLEM?

Invece di = per incarico e == per test di uguaglianza, perché non usare: = per incarico e = (o anche ==) per uguaglianza?

Osservare:

if (a=foo(bar)) {}  // obviously equality
if (a := foo(bar)) { do something with a } // obviously assignment

Se vuoi rendere più difficile per il programmatore confondere l'assegnazione per l'uguaglianza, allora rendi più difficile.

Allo stesso tempo, se volessi REALMENTE risolvere il problema, rimuoveresti il ​​coccio C che sosteneva che i booleani fossero solo numeri interi con nomi simbolici di zucchero predefiniti. Rendili del tutto diversi. Quindi, invece di dire

int a = some_value();
if (a) {}

costringi il programmatore a scrivere:

int a = some_value();
if (a /= 0) {} // Note that /= means 'not equal'.  This is your Ada lesson for today.

Il fatto è che l'assegnazione come operatore è un costrutto molto utile. Non abbiamo eliminato le lame di rasoio perché alcune persone si tagliano da sole. Invece, re Gillette ha inventato il rasoio di sicurezza.


2
(1) :=per incarico e =per l'uguaglianza potrebbe risolvere questo problema, ma a costo di alienare ogni programmatore che non è cresciuto usando un piccolo set di linguaggi non tradizionali. (2) Tipi diversi dai bool che sono consentiti in condizioni non sono sempre dovuti alla confusione di bool e interi, è sufficiente dare un'interpretazione vera / falsa ad altri tipi. Un linguaggio più recente che non ha paura di deviare da C lo ha fatto per molti tipi diversi dagli interi (ad esempio Python considera false le raccolte vuote).

1
E per quanto riguarda le lame di rasoio: servono una custodia che richiede nitidezza. D'altra parte, non sono convinto che la programmazione richieda l'assegnazione a variabili nel mezzo di una valutazione dell'espressione. Se esistesse un modo semplice, a bassa tecnologia, sicuro ed economico per far scomparire i peli del corpo senza spigoli vivi, sono sicuro che le lame di rasoio sarebbero state spostate o almeno rese molto più rare.

1
@delnan: Una volta un saggio disse: "Rendilo il più semplice possibile, ma non più semplice". Se il tuo obiettivo è quello di eliminare la stragrande maggioranza degli errori a = b vs. a == b, limitare il dominio dei test condizionali ai valori booleani ed eliminare le regole di conversione del tipo predefinito per <other> -> booleano ti porta quasi tutto Là. A quel punto, se (a = b) {} è sintatticamente legale solo se aeb sono entrambi booleani e a è un valore legale.
John R. Strohm,

Fare un incarico come una dichiarazione è almeno semplice quanto - probabilmente anche più semplice di - i cambiamenti che proponi e realizza almeno quanto - probabilmente anche di più (non consente nemmeno if (a = b)per lvalue a, boolean a, b). In una lingua senza tipizzazione statica, fornisce anche messaggi di errore molto migliori (al momento dell'analisi rispetto al tempo di esecuzione). Inoltre, prevenire "a = b vs. a == b errori" potrebbe non essere l'unico obiettivo rilevante. Ad esempio, vorrei anche consentire il codice come if items:significhi if len(items) != 0, e che avrei dovuto rinunciare a limitare le condizioni ai booleani.

1
@delnan Pascal è un linguaggio non tradizionale? Milioni di persone hanno imparato a programmare usando Pascal (e / o Modula, che deriva da Pascal). E Delphi è ancora comunemente usato in molti paesi (forse non tanto nei tuoi).
jwenting

5

Per rispondere effettivamente alla domanda, sì, ci sono numerosi usi di questo sebbene siano leggermente di nicchia.

Ad esempio in Java:

while ((Object ob = x.next()) != null) {
    // This will loop through calling next() until it returns null
    // The value of the returned object is available as ob within the loop
}

L'alternativa senza utilizzare l'assegnazione incorporata richiede la obdefinizione al di fuori dell'ambito del ciclo e due posizioni di codice separate che chiamano x.next ().

È già stato menzionato che è possibile assegnare più variabili in un solo passaggio.

x = y = z = 3;

Questo genere di cose è l'uso più comune, ma i programmatori creativi ne troveranno sempre di più.


Quella condizione del ciclo while si sposta e crea un nuovo oboggetto con ogni ciclo?
user3932000,

@ user3932000 In quel caso probabilmente no, di solito x.next () sta ripetendo qualcosa. È certamente possibile che potrebbe però.
Tim B,

1

Dal momento che riesci a inventare tutte le regole, perché ora consentire all'assegnazione di trasformare un valore e semplicemente no consentire le assegnazioni all'interno dei passaggi condizionali? Questo ti dà lo zucchero sintattico per rendere facili le inizializzazioni, evitando comunque un errore di codifica comune.

In altre parole, rendilo legale:

a=b=c=0;

Ma rendilo illegale:

if (a=b) ...

2
Sembra una regola piuttosto ad hoc. Rendere l'assegnazione una dichiarazione ed estenderla per consentire a = b = csembra più ortogonale e anche più facile da implementare. Questi due approcci non sono d'accordo sull'assegnazione nelle espressioni ( a + (b = c)), ma non hai preso posizione su quelli, quindi presumo che non contino.

"facile da implementare" non dovrebbe essere molto importante. Stai definendo un'interfaccia utente: metti al primo posto le esigenze degli utenti. Devi semplicemente chiederti se questo comportamento aiuta o ostacola l'utente.
Bryan Oakley,

se impedisci la conversione implicita in bool, allora non devi preoccuparti dell'incarico in condizioni
maniaco del cricchetto

Più facile da implementare era solo uno dei miei argomenti. E il resto? Dal punto di vista dell'interfaccia utente, potrei aggiungere che il design incoerente di IMHO e le eccezioni ad-hoc generalmente ostacolano l'utente nel trekking e nell'internalizzazione delle regole.

@ratchetfreak potresti avere ancora problemi con l'assegnazione di bool effettivi
jk.

0

A quanto pare, sei sulla strada della creazione di un linguaggio abbastanza rigoroso.

Con questo in mente, costringendo le persone a scrivere:

a=c;
b=c;

invece di:

a=b=c;

potrebbe sembrare un miglioramento per impedire alle persone di fare:

if (a=b) {

quando intendevano fare:

if (a==b) {

ma alla fine, questo tipo di errori è facile da rilevare e avvisare se si tratta o meno di un codice legale.

Tuttavia, ci sono situazioni in cui:

a=c;
b=c;

non significa questo

if (a==b) {

sarà vero.

Se c è effettivamente una funzione c (), potrebbe restituire risultati diversi ogni volta che viene chiamato. (potrebbe anche essere costoso dal punto di vista computazionale ...)

Allo stesso modo se c è un puntatore all'hardware mappato in memoria, allora

a=*c;
b=*c;

entrambi sono probabilmente diversi, e possono anche avere effetti elettronici sull'hardware ad ogni lettura.

Esistono molte altre permutazioni con l'hardware in cui è necessario essere precisi su quali indirizzi di memoria vengono letti, scritti su e sotto specifici vincoli di temporizzazione, in cui l'esecuzione di più assegnazioni sulla stessa riga è rapida, semplice e ovvia, senza i rischi di temporizzazione che introducono variabili temporanee


4
L'equivalente di a = b = cnon lo è a = c; b = c, lo è b = c; a = b. Questo evita la duplicazione degli effetti collaterali e mantiene anche la modifica ae bnello stesso ordine. Inoltre, tutti questi argomenti relativi all'hardware sono piuttosto stupidi: la maggior parte delle lingue non sono lingue di sistema e non sono progettate per risolvere questi problemi né vengono utilizzate in situazioni in cui si verificano. Questo vale doppiamente per un linguaggio che tenta di spostare JavaScript e / o PHP.

delnan, il problema non erano questi esempi inventati, lo sono. Il punto è ancora che mostrano i tipi di luoghi in cui scrivere a = b = c è comune e, nel caso dell'hardware, considerati buone pratiche, come richiesto dall'OP. Sono sicuro che saranno in grado di considerare la loro rilevanza per il loro ambiente previsto
Michael Shaw,

Guardando indietro, il mio problema con questa risposta non è principalmente quello di concentrarsi sui casi d'uso della programmazione del sistema (anche se sarebbe abbastanza brutto, come è scritto), ma che si basa sull'assumere una riscrittura errata. Gli esempi non sono esempi di luoghi in cui a=b=cè comune / utile, sono esempi di luoghi in cui è necessario occuparsi dell'ordine e del numero di effetti collaterali. Questo è del tutto indipendente. Riscrivi correttamente l' assegnazione concatenata ed entrambe le varianti sono ugualmente corrette.

@delnan: il valore viene convertito nel tipo di bin una temperatura e quello viene convertito nel tipo di ain un'altra temperatura. La tempistica relativa di quando questi valori sono effettivamente memorizzati non è specificata. Dal punto di vista della progettazione linguistica, ritengo ragionevole richiedere che tutti i valori in un'istruzione di assegnazione multipla abbiano un tipo di corrispondenza, e possibilmente richiedere anche che nessuno di essi sia volatile.
supercat,

0

Il più grande vantaggio per me di avere un'espressione come compito è che consente alla tua grammatica di essere più semplice se uno dei tuoi obiettivi è che "tutto è un'espressione" - un obiettivo di LISP in particolare.

Python non ha questo; ha espressioni e dichiarazioni, l'assegnazione è una dichiarazione. Ma poiché Python definisce una lambdaforma come una singola espressione con parametri , ciò significa che non è possibile assegnare variabili all'interno di una lambda. Questo a volte è scomodo, ma non è un problema critico, e riguarda l'unico aspetto negativo della mia esperienza di avere un incarico in una dichiarazione in Python.

Un modo per consentire a un incarico, o piuttosto all'effetto di un incarico, di essere un'espressione senza introdurre il potenziale di if(x=1)incidenti che C ha è quello di usare un letcostrutto simile a LISP , come quello (let ((x 2) (y 3)) (+ x y))che nella tua lingua potrebbe essere valutato 5. L'uso di letquesto modo non deve necessariamente essere un compito nella tua lingua, se si definisce letcome creare un ambito lessicale. Definito in questo modo, un letcostrutto potrebbe essere compilato allo stesso modo del costruire e chiamare una funzione di chiusura nidificata con argomenti.

D'altra parte, se ti preoccupi semplicemente del if(x=1)caso, ma vuoi che l'assegnazione sia un'espressione come in C, forse basterà scegliere token diversi. Cessione: x := 1o x <- 1. Confronto: x == 1. Errore di sintassi: x = 1.


1
letdifferisce dall'assegnazione in più modi rispetto all'introduzione tecnica di una nuova variabile in un nuovo ambito. Per i principianti, non ha alcun effetto sul codice esterno al letcorpo del corpo e quindi richiede di annidare ulteriormente tutto il codice (che dovrebbe usare la variabile), un significativo svantaggio nel codice pesante per le assegnazioni. Se si dovesse seguire questa strada, set!sarebbe il miglior analogo di Lisp - completamente diverso dal confronto, ma che non richiede nidificazione o un nuovo ambito.

@delnan: Mi piacerebbe vedere una combinazione di dichiarazione e assegnazione della sintassi che proibirebbe la riassegnazione ma consentirebbe la dichiarazione, fatte salve le regole che (1) la dichiarazione sarebbe legale solo per gli identificatori di dichiarazione e assegnazione, e (2 ) la dichiarazione "annulla" una variabile in tutti gli ambiti racchiusi. Pertanto, il valore di qualsiasi identificatore valido sarebbe qualunque cosa fosse stata assegnata nella precedente dichiarazione di quel nome. Sembrerebbe un po 'più bello che dover aggiungere blocchi di scoping per le variabili che vengono utilizzate solo per poche righe o dover formulare nuovi nomi per ogni variabile temporanea.
supercat

0

So che questo significherebbe che quelle funzioni a una riga che le persone C amano così tanto non funzionerebbero più. Ho pensato (con poche prove al di là della mia esperienza personale) che la stragrande maggioranza delle volte ciò è accaduto, era davvero inteso come un'operazione di confronto.

Infatti. Questa non è una novità, tutti i sottogruppi sicuri del linguaggio C hanno già fatto questa conclusione.

MISRA-C, CERT-C e così via tutti i divieti di assegnazione all'interno delle condizioni, semplicemente perché è pericoloso.

Non esiste un caso in cui il codice basato sull'assegnazione all'interno di condizioni non possa essere riscritto.


Inoltre, tali standard mettono anche in guardia contro la scrittura di codice che si basa sull'ordine di valutazione. In questo caso è possibile assegnare più incarichi su una singola riga x=y=z;. Se una riga con più assegnazioni contiene effetti collaterali (funzioni di chiamata, accesso a variabili volatili ecc.), Non puoi sapere quale effetto collaterale si verificherà per primo.

Non ci sono punti di sequenza tra la valutazione degli operandi. Quindi non possiamo sapere se la sottoespressione yviene valutata prima o dopo z: si tratta di un comportamento non specificato in C. Pertanto tale codice è potenzialmente inaffidabile, non portatile e non conforme ai citati sottogruppi sicuri di C.

La soluzione sarebbe stata quella di sostituire il codice con y=z; x=y;. Ciò aggiunge un punto di sequenza e garantisce l'ordine di valutazione.


Quindi, in base a tutti i problemi che ciò ha causato in C, qualsiasi linguaggio moderno farebbe bene sia a vietare le assegnazioni all'interno delle condizioni, sia a più assegnazioni su una singola riga.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.