scala functional-programming io assignment-operator

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



functional-programming io (8)

¿Cuál es la motivación para asignar la asignación de Scala a la Unidad 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 una cosa interesante para dejar fuera de un lenguaje funcional. Me pregunto por qué se hizo así?


Abogué por que las asignaciones devuelvan el valor asignado en lugar de la unidad. Martin y yo fuimos de un lado a otro, pero su argumento era que poner un valor en la pila solo para abrirlo el 95% del tiempo era un desperdicio de códigos de bytes y tenía un impacto negativo en el rendimiento.


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

class MyBean { private var internalState: String = _ def state = internalState def state_=(state: String) = internalState = state }

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

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


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


No estoy al tanto de la información interna sobre las razones reales, pero mi sospecha es muy simple. Scala hace que los bucles de efectos laterales sean incómodos de usar, por lo que los programadores naturalmente preferirán las comprensiones.

Lo hace de muchas maneras. Por ejemplo, no tiene un bucle for donde declara y muta una variable. No puede (fácilmente) mutar el estado en un ciclo while 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 de ella. Las variables declaradas dentro de un bloque while no son visibles desde la condición de prueba while , lo que hace do { ... } while (...) menos útil do { ... } while (...) . Y así.

Solución:

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

Por lo que sea que valga

Como explicación alternativa, tal vez Martin Odersky tuvo que enfrentar algunos errores muy feos derivados de tal uso, y decidió prohibirlo en su idioma.

EDITAR

David Pollack ha answered 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 los asuntos relacionados con el desempeño presentado por Pollack.


Por cierto: creo que el truco de while inicial es estúpido, 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 tarea aparece dos veces, pero al menos bytesRead está en el alcance al que pertenece, y no estoy jugando con trucos divertidos de asignación ...


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 de que tendrá que usar ref.value para acceder a la referencia después, puede escribir su predicado while 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 hacer la comprobación contra bytesRead de una manera más implícita sin tener que escribirlo.


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

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

Una breve ilustración de por qué CQS es útil: considere un hipotético lenguaje F / OO híbrido con una clase List que tenga los métodos Sort , Append , First y Length . En el estilo OO imperativo, 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, sería más probable escribir 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.

Sin embargo, usando CQS, insistiríamos en que si Append y Sort alteran la lista, deben devolver el tipo de unidad, lo que nos impide crear errores utilizando el segundo formulario cuando no deberíamos. La presencia de efectos secundarios, por lo tanto, también queda implícita en la firma del método.


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

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