Qual è la motivazione per l'assegnazione di Scala che valuta a Unità piuttosto che il valore assegnato?


84

Qual è la motivazione per l'assegnazione di Scala che valuta a Unità piuttosto che il valore assegnato?

Uno schema comune nella programmazione I / O è fare cose come questa:

while ((bytesRead = in.read(buffer)) != -1) { ...

Ma questo non è possibile in Scala perché ...

bytesRead = in.read(buffer)

.. restituisce Unit, non il nuovo valore di bytesRead.

Sembra una cosa interessante da lasciare fuori da un linguaggio funzionale. Mi chiedo perché è stato fatto così?


David Pollack ha pubblicato alcune informazioni di prima mano, sostenute dal commento che lo stesso Martin Odersky ha lasciato sulla sua risposta. Penso che si possa tranquillamente accettare la risposta di Pollack.
Daniel C. Sobral

Risposte:


89

Ho sostenuto che le assegnazioni restituissero il valore assegnato anziché l'unità. Martin e io siamo andati avanti e indietro su questo argomento, ma il suo argomento era che mettere un valore in pila solo per estrarlo il 95% delle volte era uno spreco di byte-code e aveva un impatto negativo sulle prestazioni.


7
C'è un motivo per cui il compilatore Scala non può controllare se il valore dell'assegnazione è effettivamente utilizzato e generare di conseguenza un bytecode efficiente?
Matt R

43
Non è così facile in presenza dei setter: ogni setter deve restituire un risultato, che è un dolore da scrivere. Quindi il compilatore deve ottimizzarlo, il che è difficile da fare tra le chiamate.
Martin Odersky

1
La tua argomentazione ha senso, ma java e C # sono contrari. Immagino che tu stia facendo qualcosa di strano con il byte code generato, quindi come sarebbe un compito in Scala compilato in un file di classe e il decompilato di nuovo in Java sarebbe simile?
Phương Nguyễn

3
@ PhươngNguyễn La differenza è il principio di accesso uniforme. In C # / Java i setter (di solito) ritornano void. In Scala foo_=(v: Foo)dovrebbe tornare Foose l'assegnazione lo fa.
Alexey Romanov

5
@ Martin Odersky: che ne dici di quanto segue: i setter rimangono void( Unit), gli incarichi x = valuevengono tradotti nell'equivalente di x.set(value);x.get(value); il compilatore elimina nelle fasi di ottimizzazione le get-chiamate se il valore era inutilizzato. Potrebbe essere un gradito cambiamento in una nuova major (a causa dell'incompatibilità con le versioni precedenti) di Scala e meno irritazioni per gli utenti. Cosa pensi?
Eugen Labun

20

Non sono a conoscenza di informazioni privilegiate sui motivi reali, ma il mio sospetto è molto semplice. Scala rende i cicli con effetti collaterali scomodi da usare in modo che i programmatori preferiscano naturalmente le comprensioni.

Lo fa in molti modi. Ad esempio, non hai un forciclo in cui dichiari e muti una variabile. Non puoi (facilmente) mutare lo stato su un whileloop nello stesso momento in cui provi la condizione, il che significa che spesso devi ripetere la mutazione appena prima e alla fine di essa. Le variabili dichiarate all'interno di un whileblocco non sono visibili dalla whilecondizione di test, il che rendedo { ... } while (...) molto meno utile. E così via.

Soluzione:

while ({bytesRead = in.read(buffer); bytesRead != -1}) { ... 

Per qualunque cosa valga.

Come spiegazione alternativa, forse Martin Odersky ha dovuto affrontare alcuni bug molto brutti derivanti da tale utilizzo e ha deciso di metterlo fuori legge dalla sua lingua.

MODIFICARE

David Pollack ha risposto con alcuni fatti concreti, che sono chiaramente confermati dal fatto che concreti lo stesso Martin Odersky ha commentato la sua risposta, dando credito all'argomento delle questioni relative alle prestazioni avanzato da Pollack.


3
Quindi presumibilmente la forversione in loop sarebbe: il for (bytesRead <- in.read(buffer) if (bytesRead) != -1che è fantastico tranne che non funzionerà perché non ce n'è foreache withFilterdisponibile!
oxbow_lakes

12

Ciò è accaduto perché Scala aveva un sistema di tipi più "formalmente corretto". Formalmente, l'assegnazione è un'affermazione puramente con effetti collaterali e quindi dovrebbe tornare Unit. Questo ha delle belle conseguenze; per esempio:

class MyBean {
  private var internalState: String = _

  def state = internalState

  def state_=(state: String) = internalState = state
}

Il state_=metodo restituisce Unit(come ci si aspetterebbe da un setter) proprio perché l'assegnazione ritorna Unit.

Sono d'accordo sul fatto che per i modelli in stile C come la copia di un flusso o simili, questa particolare decisione di progettazione può essere un po 'problematica. Tuttavia, in realtà è relativamente non problematico in generale e contribuisce davvero alla coerenza complessiva del sistema di tipi.


Grazie, Daniel. Penso che lo preferirei se la coerenza fosse che entrambi gli incarichi E i setter restituissero il valore! (Non c'è motivo per cui non possano farlo.) Sospetto di non cogliere ancora le sfumature dei concetti come una "affermazione puramente con effetto collaterale".
Graham Lea

2
@Graham: Ma poi, dovresti seguire la coerenza e assicurarti in tutti i tuoi setter, per quanto complessi possano essere, che restituiscano il valore che hanno impostato. Questo sarebbe complicato in alcuni casi e in altri casi semplicemente sbagliato, penso. (Cosa restituiresti in caso di errore? Null? - piuttosto no. Nessuno? - quindi il tuo tipo sarà Opzione [T].) Penso che sia difficile essere coerenti con questo.
Debilski

7

Forse ciò è dovuto al principio di separazione tra query e comandi ?

CQS tende ad essere popolare all'intersezione di OO e stili di programmazione funzionale, poiché crea un'ovvia distinzione tra metodi oggetto che hanno o non hanno effetti collaterali (cioè che alterano l'oggetto). L'applicazione di CQS alle assegnazioni di variabili sta andando oltre il solito, ma vale la stessa idea.

Una breve illustrazione del perché CQS è utile: si consideri un linguaggio F / OO ipotetico ibrido con una Listclasse che ha metodi Sort, Append, First, e Length. In imperativo stile OO, si potrebbe voler scrivere una funzione come questa:

func foo(x):
    var list = new List(4, -2, 3, 1)
    list.Append(x)
    list.Sort()
    # list now holds a sorted, five-element list
    var smallest = list.First()
    return smallest + list.Length()

Considerando che in uno stile più funzionale, è più probabile scrivere qualcosa del genere:

func bar(x):
    var list = new List(4, -2, 3, 1)
    var smallest = list.Append(x).Sort().First()
    # list still holds an unsorted, four-element list
    return smallest + list.Length()

Questi sembrano tentare di fare la stessa cosa, ma ovviamente uno dei due non è corretto e, senza saperne di più sul comportamento dei metodi, non possiamo dire quale.

Usando CQS, tuttavia, insisteremmo sul fatto che se Appende Sortalteriamo l'elenco, devono restituire il tipo di unità, impedendoci così di creare bug usando la seconda forma quando non dovremmo. La presenza di effetti collaterali diventa quindi implicita anche nella firma del metodo.


4

Immagino che questo sia per mantenere il programma / il linguaggio privo di effetti collaterali.

Quello che descrivi è l'uso intenzionale di un effetto collaterale che nel caso generale è considerato una cosa negativa.


Eh. Scala priva di effetti collaterali? :) Inoltre, immaginate un caso come val a = b = 1(immaginate "magica" valdi fronte b) vs. val a = 1; val b = 1;.

Questo non ha nulla a che fare con gli effetti collaterali, almeno non nel senso qui descritto: Effetto collaterale (informatica)
Feuermurmel

4

Non è lo stile migliore utilizzare un compito come espressione booleana. Esegui due cose contemporaneamente, il che porta spesso a errori. E l'uso accidentale di "=" invece di "==" è evitato con la restrizione di Scalas.


2
Penso che questo sia un motivo da schifo! Come pubblicato dall'OP, il codice viene comunque compilato ed eseguito: semplicemente non fa quello che ci si potrebbe ragionevolmente aspettare. È un altro trucchetto, non uno di meno!
oxbow_lakes

1
Se scrivi qualcosa come if (a = b) non verrà compilato. Quindi almeno questo errore può essere evitato.
deamon

1
L'OP non ha usato "=" invece di "==", ha usato entrambi. Si aspetta che l'assegnazione restituisca un valore che può essere utilizzato, ad esempio, per il confronto con un altro valore (-1 nell'esempio)
IttayD

@deamon: verrà compilato (almeno in Java) se aeb sono booleani. Ho visto i neofiti cadere su questa trappola usando if (a = true). Un motivo in più per preferire il più semplice if (a) (e più chiaro se si usa un nome più significativo!).
PhiLho

2

A proposito: trovo il trucco iniziale stupido, anche in Java. Perché non qualcosa di simile?

for(int bytesRead = in.read(buffer); bytesRead != -1; bytesRead = in.read(buffer)) {
   //do something 
}

Certo, l'assegnazione appare due volte, ma almeno bytesRead rientra nell'ambito a cui appartiene, e non sto giocando con divertenti trucchi di assegnazione ...


1
Anche se il trucco è piuttosto comune, di solito appare in ogni app che legge attraverso un buffer. E sembra sempre la versione di OP.
TWiStErRob

0

Puoi avere una soluzione alternativa per questo, purché tu abbia un tipo di riferimento per l'indirizzamento indiretto. In un'implementazione ingenua, puoi usare quanto segue per i tipi arbitrari.

case class Ref[T](var value: T) {
  def := (newval: => T)(pred: T => Boolean): Boolean = {
    this.value = newval
    pred(this.value)
  }
}

Quindi, sotto il vincolo che dovrai utilizzare ref.valueper accedere al riferimento in seguito, puoi scrivere il tuo whilepredicato come

val bytesRead = Ref(0) // maybe there is a way to get rid of this line

while ((bytesRead := in.read(buffer)) (_ != -1)) { // ...
  println(bytesRead.value)
}

e puoi fare il controllo bytesReadin un modo più implicito senza doverlo digitare.

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.