2010-01-04 16 views
67

Qual è la motivazione per l'assegnazione di Scala alla valutazione dell'unità anziché al valore assegnato?Qual è la motivazione per l'assegnazione di Scala alla valutazione dell'unità piuttosto che il valore assegnato?

un modello comune nella programmazione di I/O è quello di fare le cose in questo modo:

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

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

bytesRead = in.read(buffer) 

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

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

+0

David Pollack ha pubblicato alcune informazioni di prima mano, praticamente approvate dal commento che lo stesso Martin Odersky ha lasciato sulla sua risposta. Penso che si possa tranquillamente recepire la risposta di Pollack. –

risposta

66

Ho sostenuto che le assegnazioni restituiscono il valore assegnato anziché l'unità. Martin e io siamo andati avanti e indietro, ma la sua argomentazione era che mettere un valore in pila solo per sbarazzarsi del 95% delle volte era uno spreco di codici byte e avere un impatto negativo sulle prestazioni.

+4

C'è un motivo per cui la Scala il compilatore non ha potuto verificare se il valore dell'assegnamento è effettivamente utilizzato e generare di conseguenza un codice bytecode efficiente? –

+39

Non è così semplice in presenza di setter: ogni setter deve restituire un risultato, che è un dolore da scrivere. Quindi il compilatore deve ottimizzarlo, il che è difficile da fare attraverso le chiamate. –

+0

Il tuo argomento ha senso, eppure java e C# sono contrari. Immagino che stai facendo qualcosa di strano con il codice byte generato, allora come sarebbe un compito in Scala essere compilato in un file di classe e il decompilato di nuovo a Java? –

5

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.

+0

Heh. Scala privo di effetti collaterali? :) Inoltre, immagina un caso come "val a = b = 1' (immagina" magico "' val' davanti a 'b') contro' val a = 1; val b = 1; '. –

4

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

+2

Penso che questo sia un motivo per la spazzatura! Dato che l'OP ha pubblicato, il codice continua a compilarlo ed esegue: semplicemente non fa ciò che potresti ragionevolmente aspettarti. È un altro trucchetto, non uno in meno! –

+1

Se scrivi qualcosa come se (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 quindi utilizzato, ad esempio, per confrontare un altro valore (-1 nell'esempio) – IttayD

16

Non sono al corrente di informazioni interne sulle reali ragioni, ma il mio sospetto è molto semplice. Scala rende scomodi i loop con effetti collaterali, in modo che i programmatori preferiscano naturalmente le incomprensioni.

Lo fa in molti modi. Ad esempio, non hai un ciclo for in cui dichiari e muti una variabile. Non è possibile (facilmente) modificare lo stato su un ciclo while mentre si verifica la condizione, il che significa che spesso è necessario ripetere la mutazione prima di essa e alla fine di essa. Le variabili dichiarate all'interno di un blocco while non sono visibili dalla condizione di test while, il che rende do { ... } while (...) molto meno utile. E così via.

Soluzione:

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

Per quel che vale la pena.

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.

EDIT

David Pollack ha answered con alcuni fatti reali, che sono chiaramente approvati dal fatto che lo stesso Martin Odersky commentato la sua risposta, dando credito alla tesi problemi relativi alle prestazioni portata avanti da Pollack.

+3

Quindi presumibilmente la versione di 'for' loop sarebbe:' for (bytesRead <- in.read (buffer) if (bytesRead)! = -1' che è grande tranne che non funzionerà perché non c'è 'foreach' e 'withFilter' available! –

8

Questo è avvenuto come parte di Scala con un sistema di tipo più "formalmente corretto". Formalmente parlando, l'assegnazione è una dichiarazione puramente collaterale e quindi dovrebbe restituire Unit.Questo ha delle belle conseguenze; ad esempio:

class MyBean { 
    private var internalState: String = _ 

    def state = internalState 

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

Procedimento state_= restituisce Unit (come ci si aspetterebbe per un setter) proprio perché assegnazione restituisce Unit.

Sono d'accordo 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 realmente alla coerenza complessiva del sistema di tipi.

+0

Grazie, Daniel. Penso che lo preferirei se la consistenza fosse che entrambi gli incarichi e setter hanno restituito il valore! (Non c'è ragione per cui non possano farlo). Sospetto che non sto facendo il groking con le sfumature dei concetti come una "dichiarazione puramente side-effecting" ancora –

+2

@Graham: Ma poi, dovresti seguire la coerenza e assicurarti in tutti i setter per quanto complessi possano essere, che restituiscano il valore che hanno impostato. in alcuni casi e in altri casi solo in modo sbagliato, penso. (Cosa vorresti restituire in caso di errore? null? - piuttosto no. Nessuno? - allora il tuo tipo sarà Option [T].) Penso che sia difficile essere coerenti con quello – Debilski

5

Forse questo è dovuto al principio command-query separation?

CQS tende ad essere popolare all'intersezione di OO e degli stili di programmazione funzionale, in quanto crea un'evidente 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 si applica la stessa idea.

Una breve illustrazione del perché CQS è utile: si consideri una lingua ipotetica ibrido F/OO con una classe List che ha metodi Sort, Append, First e Length. In stile OO imperativo, 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() 

Mentre in stile più funzionale, si farebbe più probabile scrivere qualcosa di simile:

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 essere cercare fare la stessa cosa, ma ovviamente uno dei due è errato, e senza sapere di più sul comportamento dei metodi, non possiamo dire quale.

Utilizzando CQS, tuttavia, ci sarebbe insistere sul fatto che, se Append e Sort modificare l'elenco, devono restituire il tipo di unità, impedendo così a noi di creare bug utilizzando la seconda forma quando non dovrebbe. La presenza di effetti collaterali diventa quindi implicita anche nella firma del metodo.

2

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

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

Certo, l'assegnazione appare due volte, ma almeno bytesRead è nel campo di applicazione a cui appartiene, e non sto giocando con i trucchi di assegnazione divertenti ...

+0

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

1

Si può avere una soluzione per questo finché hai un tipo di riferimento per indiretta. In un'implementazione ingenua, è possibile utilizzare 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) 
    } 
} 

Poi, sotto il vincolo che dovrete utilizzare per accedere ref.value il riferimento in seguito, è possibile scrivere il while predicato 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 si può fare il controllo contro bytesRead in un più implicito senza doverlo digitare.

Problemi correlati