¿Cuál es la motivación para evaluar la asignación de Scala a Unit en lugar del valor asignado?

84

¿Cuál es la motivación para evaluar la asignación de Scala a Unit en lugar del valor asignado?

Un patrón común en la programación de E / S es hacer cosas como esta:

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

Pero esto no es posible en Scala porque ...

bytesRead = in.read(buffer)

.. devuelve Unit, no el nuevo valor de bytesRead.

Parece algo interesante que dejar fuera de un lenguaje funcional. Me pregunto por qué se hizo así.

Graham Lea
fuente
David Pollack ha publicado información de primera mano, prácticamente respaldada por el comentario que el propio Martin Odersky dejó en su respuesta. Creo que uno puede aceptar con seguridad la respuesta de Pollack.
Daniel C. Sobral

Respuestas:

88

Abogué por que las asignaciones devuelvan el valor asignado en lugar de la unidad. Martin y yo íbamos y veníamos sobre él, pero su argumento era que poner un valor en la pila solo para sacarlo el 95% del tiempo era una pérdida de códigos de bytes y tenía un impacto negativo en el rendimiento.

David Pollak
fuente
7
¿Hay alguna razón por la que el compilador de Scala no pueda ver si el valor de la asignación se usa realmente y generar un código de bytes eficiente en consecuencia?
Matt R
43
No es tan fácil en presencia de colocadores: cada colocador tiene que devolver un resultado, que es una molestia para escribir. Luego, el compilador tiene que optimizarlo, lo que es difícil de hacer entre llamadas.
Martin Odersky
1
Su argumento tiene sentido, pero Java & C # están en contra de eso. Supongo que está haciendo algo extraño con el código de bytes generado, entonces, ¿cómo se vería una asignación en Scala compilada en un archivo de clase y descompilada de nuevo en Java?
Phương Nguyễn
3
@ PhươngNguyễn La diferencia es el principio de acceso uniforme. En C # / Java setters (generalmente) return void. En Scala foo_=(v: Foo)debería volver Foosi lo hace la asignación.
Alexey Romanov
5
@Martin Odersky: qué tal lo siguiente: los establecedores permanecen void( Unit), las asignaciones x = valuese traducen al equivalente de x.set(value);x.get(value); el compilador elimina en las fases de optimización las get-calls si el valor no se usó. Podría ser un cambio bienvenido en una nueva versión importante de Scala (debido a la incompatibilidad con versiones anteriores) y menos irritaciones para los usuarios. ¿Qué piensas?
Eugen Labun
20

No tengo acceso a información privilegiada sobre las razones reales, pero mi sospecha es muy simple. Scala hace que los bucles de efectos secundarios sean incómodos de usar, por lo que los programadores naturalmente preferirán las comprensiones.

Lo hace de muchas formas. Por ejemplo, no tiene un forbucle en el que declare y mute una variable. No puede (fácilmente) mutar un estado en un whilebucle al mismo tiempo que prueba la condición, lo que significa que a menudo tiene que repetir la mutación justo antes y al final. Las variables declaradas dentro de un whilebloque no son visibles desde la whilecondición de prueba, lo que las hace do { ... } while (...)mucho menos útiles. Y así.

Solución alterna:

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

Por lo que valga.

Como explicación alternativa, quizás Martin Odersky tuvo que enfrentarse a algunos errores muy feos derivados de tal uso y decidió prohibirlo en su lenguaje.

EDITAR

David Pollack ha respondido con algunos hechos reales, que están claramente respaldados por el hecho de que el propio Martin Odersky comentó su respuesta, dando crédito al argumento de cuestiones relacionadas con el desempeño presentado por Pollack.

Daniel C. Sobral
fuente
3
Entonces, presumiblemente, la forversión de bucle sería: lo for (bytesRead <- in.read(buffer) if (bytesRead) != -1cual es genial, excepto que no funcionará porque no hay foreachy está withFilterdisponible.
oxbow_lakes
12

Esto sucedió como parte de Scala que tenía un sistema de tipos más "formalmente correcto". Hablando formalmente, la asignación es una declaración puramente de efectos secundarios y, por lo tanto, debe regresar Unit. Esto tiene algunas buenas consecuencias; por ejemplo:

class MyBean {
  private var internalState: String = _

  def state = internalState

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

El state_=método regresa Unit(como se esperaría de un establecedor) precisamente porque la asignación regresa Unit.

Estoy de acuerdo en que para patrones de estilo C como copiar una secuencia o similar, esta decisión de diseño en particular puede ser un poco problemática. Sin embargo, en realidad es relativamente poco problemático en general y realmente contribuye a la consistencia general del sistema de tipos.

Daniel Spiewak
fuente
Gracias, Daniel. ¡Creo que preferiría que la coherencia fuera que tanto las asignaciones como los establecedores devolvieran el valor! (No hay ninguna razón por la que no puedan.) Sospecho que no estoy asimilando los matices de los conceptos como una "declaración puramente de efectos secundarios" por el momento.
Graham Lea
2
@Graham: Pero entonces, tendrías que seguir la consistencia y asegurarte de que todos tus setters, por complejos que sean, devuelvan el valor que establecieron. Esto sería complicado en algunos casos y en otros casos simplemente incorrecto, creo. (¿Qué devolvería en caso de error? ¿Null? - en lugar de no. ¿Ninguno? - entonces su tipo será Option [T].) Creo que es difícil ser coherente con eso.
Debilski
7

¿Quizás esto se deba al principio de separación comando-consulta ?

CQS tiende a ser popular en la intersección de OO y estilos de programación funcional, ya que crea una distinción obvia entre métodos de objeto que tienen o no efectos secundarios (es decir, que alteran el objeto). La aplicación de CQS a las asignaciones de variables va más allá de lo habitual, pero se aplica la misma idea.

Un corto ilustración de por qué CQS es útil: Considere un lenguaje F / OO híbrido hipotético con una Listclase que tiene métodos Sort, Append, First, y Length. En imperativo estilo OO, uno podría querer escribir una función como esta:

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()

Mientras que en un estilo más funcional, es más probable que uno escriba algo como esto:

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()

Estos parecen estar intentando hacer lo mismo, pero obviamente uno de los dos es incorrecto y sin saber más sobre el comportamiento de los métodos, no podemos decir cuál.

El uso de SCC, sin embargo, nos gustaría insistir en que si Appendy Sortmodificar la lista, deben devolver el tipo de unidad, lo que nos impide la creación de errores mediante el uso de la segunda forma cuando no debería. Por lo tanto, la presencia de efectos secundarios también se vuelve implícita en la firma del método.

CA McCann
fuente
4

Supongo que esto es para mantener el programa / lenguaje libre de efectos secundarios.

Lo que describe es el uso intencional de un efecto secundario que en el caso general se considera algo malo.

Jens Schauder
fuente
Je. ¿Scala libre de efectos secundarios? :) Además, imaginar un caso como val a = b = 1(imaginar "mágica" valfrente b) vs val a = 1; val b = 1;.
Esto no tiene nada que ver con los efectos secundarios, al menos no en el sentido que se describe aquí: Efecto secundario (ciencias de la computación)
Feuermurmel
4

No es el mejor estilo utilizar una asignación como expresión booleana. Realiza dos cosas al mismo tiempo, lo que a menudo conduce a errores. Y el uso accidental de "=" en lugar de "==" se evita con la restricción de Scalas.

demonio
fuente
2
¡Creo que esta es una razón tonta! Como publicó el OP, el código aún se compila y se ejecuta: simplemente no hace lo que razonablemente podría esperar. ¡Es uno más, no uno menos!
oxbow_lakes
1
Si escribe algo como si (a = b) no se compilará. Entonces, al menos, este error se puede evitar.
demonio
1
El OP no usó '=' en lugar de '==', usó ambos. Él espera que la asignación devuelva un valor que luego se puede usar, por ejemplo, para comparar con otro valor (-1 en el ejemplo)
IttayD
@deamon: se compilará (al menos en Java) si ayb son booleanos. He visto a novatos caer en esta trampa usando if (a = true). Una razón más para preferir el if más simple (a) (¡y más claro si usa un nombre más significativo!).
PhiLho
2

Por cierto: encuentro estúpido el truco inicial inicial, incluso en Java. ¿Por qué no algo así?

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

Por supuesto, la asignación aparece dos veces, pero al menos bytesRead está en el ámbito al que pertenece, y no estoy jugando con trucos divertidos de asignación ...

Landei
fuente
1
Que si bien el truco es bastante común, generalmente aparece en todas las aplicaciones que se leen a través de un búfer. Y siempre se parece a la versión de OP.
TWiStErRob
0

Puede tener una solución para esto siempre que tenga un tipo de referencia para la indirección. En una implementación ingenua, puede usar lo siguiente para tipos arbitrarios.

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

Luego, bajo la restricción que tendrá que usar ref.valuepara acceder a la referencia posteriormente, puede escribir su whilepredicado como

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

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

y puede realizar la comprobación bytesReadde una manera más implícita sin tener que escribirlo.

Debilski
fuente