¿Cuál es el beneficio de que el operador de asignación devuelva un valor?


27

Estoy desarrollando un lenguaje que tengo la intención de reemplazar tanto Javascript como PHP. (No veo ningún problema con esto. No es que ninguno de estos idiomas tenga una gran base de instalación).

Una de las cosas que quería cambiar era convertir el operador de asignación en un comando de asignación, eliminando la capacidad de hacer uso del valor devuelto.

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

Sé que esto significaría que esas funciones de una línea que las personas C aman tanto ya no funcionarían. Pensé (con poca evidencia más allá de mi experiencia personal) que la gran mayoría de las veces que esto sucedió, realmente tenía la intención de ser una operación de comparación.

¿O es eso? ¿Hay algún uso práctico del valor de retorno del operador de asignación que no pueda ser reescrito trivialmente? (Para cualquier idioma que tenga dicho concepto).


12
JS y PHP no tienen una gran "base de instalación"?
mhr

47
@mri sospecho de sarcasmo.
Andy Hunt

12
El único caso útil que puedo recordar es while((x = getValue()) != null) {}. Los reemplazos serán más feos ya que necesitarás usar breako repetir la x = getValuetarea.
CodesInChaos

12
@mri Ooh no, escuché que esos dos idiomas son cosas triviales sin ninguna inversión significativa. Una vez que las pocas personas que insisten en usar JS ven mi idioma, cambiarán al mío y nunca más tendrán que escribir ===. Estoy igualmente seguro de que los fabricantes de navegadores lanzarán inmediatamente una actualización que incluye mi idioma junto con JS. :)
billpg

44
Le sugeriría que si su intención es mejorar un idioma existente y tiene la intención de que se adopte ampliamente, entonces la compatibilidad con el idioma existente al 100% es buena. Ver TypeScript como un ejemplo. Si su intención es proporcionar una mejor alternativa a un idioma existente, entonces tiene un problema mucho más difícil. Un nuevo idioma tiene que resolver un problema realista existente mucho mejor que el idioma existente para pagar el costo del cambio. Aprender un idioma es una inversión y debe ser rentable.
Eric Lippert

Respuestas:


25

Técnicamente, puede valer la pena conservar un poco de azúcar sintáctico, incluso si se puede reemplazar trivialmente, si mejora la legibilidad de alguna operación común. Pero la asignación como expresión no cae dentro de eso. El peligro de escribirlo en lugar de una comparación significa que rara vez se usa (a veces incluso está prohibido por las guías de estilo) y provoca una doble toma cada vez que se usa. En otras palabras, los beneficios de legibilidad son pequeños en número y magnitud.

Un vistazo a los idiomas existentes que hacen esto puede valer la pena.

  • Java y C # mantienen la asignación como una expresión, pero eliminan la trampa que mencionas al requerir condiciones para evaluar los valores booleanos. En general, esto parece funcionar bien, aunque las personas se quejan ocasionalmente de que esto no permite condiciones como if (x)en lugar if (x != null)o if (x != 0)dependiendo del tipo de x.
  • Python hace de la asignación una declaración adecuada en lugar de una expresión. Las propuestas para cambiar esto ocasionalmente llegan a la lista de correo de ideas de python, pero mi impresión subjetiva es que esto ocurre con menos frecuencia y genera menos ruido cada vez en comparación con otras características "faltantes" como bucles do-while, declaraciones de cambio, lambdas de varias líneas, etc.

Sin embargo, Python permite un caso especial, asignando a varios nombres a la vez: a = b = c. Esto se considera una declaración equivalente a b = c; a = b, y se usa ocasionalmente, por lo que puede valer la pena agregarlo a su idioma también (pero no me preocuparía, ya que esta adición debería ser compatible con versiones anteriores).


55
+1 por mencionar a = b = cque las otras respuestas realmente no mencionan.
Leo

66
Una tercera resolución es usar un símbolo diferente para la asignación. Pascal utiliza :=para la asignación.
Brian

55
@Brian: De hecho, al igual que C #. =es asignación, ==es comparación.
Marjan Venema

3
En C #, algo así if (a = true)arrojará una advertencia C4706 ( The test value in a conditional expression was the result of an assignment.). GCC con C también arrojará a warning: suggest parentheses around assignment used as truth value [-Wparentheses]. Esas advertencias pueden silenciarse con un conjunto adicional de paréntesis, pero están ahí para alentar a indicar explícitamente que la asignación fue intencional.
Bob

2
@delnan Sólo un comentario más bien genérica, sino que fue provocado por "eliminar el peligro que mencionas al exigir condiciones de evaluar a los booleanos" - a = true no evaluar a un Booleano y por lo tanto no es un error, sino que también plantea una advertencia relacionada en C #.
Bob

11

¿Hay algún uso práctico del valor de retorno del operador de asignación que no pueda ser reescrito trivialmente?

En general, no. La idea de que el valor de una expresión de asignación sea el valor asignado significa que tenemos una expresión que puede usarse tanto para su efecto secundario como para su valor , y que muchos consideran confusa.

Usos comunes son típicamente para hacer expresiones compactas:

x = y = z;

tiene la semántica en C # de "convertir z al tipo de y, asignar el valor convertido a y, el valor convertido es el valor de la expresión, convertir eso al tipo de x, asignar a x".

Pero ya estamos en el ámbito de los efectos secundarios impertativos en un contexto de declaración, por lo que realmente hay muy poco beneficio convincente sobre eso

y = z;
x = y;

De manera similar con

M(x = 123);

ser una taquigrafía para

x = 123;
M(x);

Nuevamente, en el código original estamos usando una expresión tanto por sus efectos secundarios como por su valor, y estamos haciendo una declaración que tiene dos efectos secundarios en lugar de uno. Ambos son malolientes; trate de tener un efecto secundario por enunciado y use expresiones para sus valores, no para sus efectos secundarios.

Estoy desarrollando un lenguaje que tengo la intención de reemplazar tanto Javascript como PHP.

Si realmente quiere ser audaz y enfatizar que la asignación es una declaración y no una igualdad, entonces mi consejo es: que sea claramente una declaración de asignación .

let x be 1;

El rojo. O

x <-- 1;

o mejor:

1 --> x;

O incluso mejor aún

1 → x;

No hay absolutamente ninguna forma de confundir ninguno de esos x == 1.


1
¿Está preparado el mundo para símbolos Unicode no ASCII en lenguajes de programación?
billpg

Por mucho que me encante lo que sugieres, uno de mis objetivos es que la mayoría de JavaScript "bien escrito" se pueda transferir con poca o ninguna modificación.
billpg

2
@billpg: ¿Está preparado el mundo ? No sé, ¿estaba el mundo listo para APL en 1964, décadas antes de la invención de Unicode? Aquí hay un programa en APL que selecciona una permutación aleatoria de seis números de los primeros 40: x[⍋x←6?40] APL requirió su propio teclado especial, pero fue un lenguaje bastante exitoso.
Eric Lippert

@billpg: Macintosh Programmer's Workshop utilizó símbolos que no son ASCII para cosas como etiquetas regex o redirección de stderr. Por otro lado, MPW tenía la ventaja de que Macintosh facilitaba la escritura de caracteres no ASCII. Debo confesar algunas dudas sobre por qué el controlador de teclado de EE. UU. No proporciona ningún medio decente para escribir caracteres que no sean ASCII. La entrada del número Alt no solo requiere buscar códigos de caracteres, en muchas aplicaciones ni siquiera funciona.
supercat

Hm, ¿por qué preferiría asignar "a la derecha" como a+b*c --> x? Esto me parece extraño.
Ruslan

9

Muchos idiomas eligen la ruta para hacer que la asignación sea una declaración en lugar de una expresión, incluido Python:

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

y Golang:

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

Otros idiomas no tienen asignación, sino enlaces de ámbito, por ejemplo, OCaml:

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

Sin embargo, letes una expresión en sí misma.

La ventaja de permitir la asignación es que podemos verificar directamente el valor de retorno de una función dentro del condicional, por ejemplo, en este fragmento de Perl:

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

Perl también incluye la declaración a ese condicional solamente, lo que lo hace muy útil. También advertirá si asigna dentro de un condicional sin declarar una nueva variable allí; if ($foo = $bar)advertirá, if (my $foo = $bar)no lo hará.

Hacer la asignación en otra declaración suele ser suficiente, pero puede traer problemas de alcance:

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 depende en gran medida de los valores de retorno para la comprobación de errores. Por lo tanto, permite que un condicional tome una declaración de inicialización:

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

Otros lenguajes usan un sistema de tipos para no permitir expresiones no booleanas dentro de un condicional:

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

Por supuesto, eso falla cuando se usa una función que devuelve un valor booleano.

Ahora hemos visto diferentes mecanismos para defenderse contra la asignación accidental:

  • No permitir la asignación como expresión
  • Usar comprobación de tipo estático
  • La asignación no existe, solo tenemos letenlaces
  • Permitir una declaración de inicialización, no permitir la asignación de lo contrario
  • No permitir la asignación dentro de un condicional sin declaración

Los he clasificado en orden de preferencia ascendente: las asignaciones dentro de las expresiones pueden ser útiles (y es simple evitar los problemas de Python al tener una sintaxis de declaración explícita y una sintaxis de argumento con nombre diferente). Pero está bien no permitirlos, ya que hay muchas otras opciones para el mismo efecto.

El código sin errores es más importante que el código conciso.


+1 para "No permitir asignación como expresión". Los casos de uso para la asignación como expresión no justifican la posibilidad de errores y problemas de legibilidad.
meter

7

Dijiste "Pensé (con poca evidencia más allá de mi experiencia personal) que la gran mayoría de las veces que esto sucedió, realmente tenía la intención de ser una operación de comparación".

¿Por qué no arreglar el problema?

En lugar de = para asignación y == para prueba de igualdad, ¿por qué no usar: = para asignación y = (o incluso ==) para igualdad?

Observar:

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

Si desea dificultar que el programador confunda la asignación con la igualdad, entonces hágalo más difícil.

Al mismo tiempo, si REALMENTE quisiera solucionar el problema, eliminaría la vasija C que afirmaba que los booleanos solo eran enteros con nombres simbólicos predefinidos de azúcar. Hazlos un tipo completamente diferente. Entonces, en lugar de decir

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

obligas al programador a escribir:

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

El hecho es que la asignación como operador es una construcción muy útil. No eliminamos las cuchillas de afeitar porque algunas personas se cortaron. En cambio, el rey Gillette inventó la navaja de seguridad.


2
(1) la :=asignación y la =igualdad podrían solucionar este problema, pero a costa de alienar a todos los programadores que no crecieron utilizando un pequeño conjunto de lenguajes no convencionales. (2) Los tipos que no son permitidos en bools en condiciones no siempre se deben a mezclar bools y enteros, es suficiente para dar una interpretación verdadera / falsa a otros tipos. Un lenguaje más nuevo que no tiene miedo de desviarse de C lo ha hecho para muchos tipos distintos de los enteros (por ejemplo, Python considera falsas las colecciones vacías).

1
Y con respecto a las cuchillas de afeitar: sirven para un caso de uso que requiere nitidez. Por otro lado, no estoy convencido de que programar bien requiera asignar variables a la mitad de una evaluación de expresión. Si hubiera una manera simple, de baja tecnología, segura y rentable de hacer desaparecer el vello corporal sin bordes afilados, estoy seguro de que las cuchillas de afeitar se habrían desplazado o al menos se habrían vuelto mucho más raras.

1
@delnan: Un hombre sabio dijo una vez: "Hazlo lo más simple posible, pero no más simple". Si su objetivo es eliminar la gran mayoría de los errores a = b vs. a == b, restringir el dominio de las pruebas condicionales a booleanos y eliminar las reglas de conversión de tipo predeterminadas para <otro> -> booleano lo lleva casi por completo allí. En ese punto, si (a = b) {} solo es sintácticamente legal si a y b son ambos booleanos y a es un valor legal.
John R. Strohm

Hacer que la asignación sea una declaración es al menos tan simple como, posiblemente incluso más simple que, los cambios que propone, y logra al menos lo mismo, posiblemente incluso más (ni siquiera permite if (a = b)lvalue a, boolean a, b). En un lenguaje sin tipeo estático, también ofrece mensajes de error mucho mejores (en tiempo de análisis vs. tiempo de ejecución). Además, prevenir "a = b vs. a == b errores" puede no ser el único objetivo relevante. Por ejemplo, también me gustaría permitir que el código quisiera if items:significar if len(items) != 0, y que tendría que renunciar para restringir las condiciones a los booleanos.

1
@delnan Pascal es un lenguaje no convencional? Millones de personas aprendieron programación usando Pascal (y / o Modula, que deriva de Pascal). Y Delphi todavía se usa comúnmente en muchos países (tal vez no tanto en los suyos).
Jwenting

5

Para responder realmente la pregunta, sí, hay numerosos usos de esto, aunque son un poco nicho.

Por ejemplo en 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
}

La alternativa sin usar la asignación incrustada requiere lo obdefinido fuera del alcance del bucle y dos ubicaciones de código separadas que llaman a x.next ().

Ya se ha mencionado que puede asignar múltiples variables en un solo paso.

x = y = z = 3;

Este tipo de cosas es el uso más común, pero los programadores creativos siempre encontrarán más.


¿Esa condición de bucle while desasignará y creará un nuevo obobjeto con cada bucle?
user3932000

@ user3932000 En ese caso, probablemente no, generalmente x.next () está iterando sobre algo. Sin embargo, es posible que así sea.
Tim B

1

Ya que puedes inventar todas las reglas, ¿por qué ahora permitir que la asignación convierta un valor, y simplemente no permitir asignaciones dentro de pasos condicionales? Esto le proporciona el azúcar sintáctico para facilitar las inicializaciones, al tiempo que evita un error de codificación común.

En otras palabras, legalice esto:

a=b=c=0;

Pero haz esto ilegal:

if (a=b) ...

2
Esa parece una regla bastante ad-hoc. Hacer que la asignación sea una declaración y extenderla para permitir a = b = cparece más ortogonal y también más fácil de implementar. Estos dos enfoques no están de acuerdo acerca de la asignación en las expresiones ( a + (b = c)), pero usted no ha tomado partido sobre ellos, así que supongo que no importan.

"fácil de implementar" no debería ser una gran consideración. Está definiendo una interfaz de usuario; ponga primero las necesidades de los usuarios. Simplemente debe preguntarse si este comportamiento ayuda o dificulta al usuario.
Bryan Oakley

si no permite la conversión implícita a bool, entonces no tiene que preocuparse por la asignación de condiciones
ratchet freak

Más fácil de implementar fue solo uno de mis argumentos. ¿Qué pasa con el resto? Desde el ángulo de la interfaz de usuario, podría agregar que, en mi humilde opinión, el diseño incoherente y las excepciones ad-hoc generalmente obstaculizan al usuario a asimilar e internalizar las reglas.

@ratchetfreak aún podría tener un problema con la asignación de bools reales
jk.

0

Por lo que parece, estás en el camino de crear un lenguaje bastante estricto.

Con eso en mente, obligar a las personas a escribir:

a=c;
b=c;

en lugar de:

a=b=c;

puede parecer una mejora para evitar que las personas hagan:

if (a=b) {

cuando tenían la intención de hacer:

if (a==b) {

pero al final, este tipo de errores son fáciles de detectar y advertir si son o no códigos legales.

Sin embargo, hay situaciones en las que hacer:

a=c;
b=c;

no significa eso

if (a==b) {

será verdad

Si c es realmente una función c (), entonces podría devolver resultados diferentes cada vez que se llama. (también podría ser computacionalmente costoso también ...)

Del mismo modo, si c es un puntero al hardware mapeado en memoria, entonces

a=*c;
b=*c;

Es probable que ambos sean diferentes, y también pueden tener efectos electrónicos en el hardware en cada lectura.

Hay muchas otras permutaciones con hardware en las que debe ser preciso sobre qué direcciones de memoria se leen, se escriben y bajo restricciones de tiempo específicas, donde hacer múltiples asignaciones en la misma línea es rápido, simple y obvio, sin los riesgos de tiempo que introducir variables temporales


44
El equivalente a a = b = cno es a = c; b = c, es b = c; a = b. Esto evita la duplicación de efectos secundarios y también mantiene la modificación ay ben el mismo orden. Además, todos estos argumentos relacionados con el hardware son un poco estúpidos: la mayoría de los idiomas no son lenguajes de sistema y no están diseñados para resolver estos problemas ni se utilizan en situaciones en las que ocurren estos problemas. Esto va doblemente para un lenguaje que intenta desplazar JavaScript y / o PHP.

Delnan, el problema no son estos ejemplos artificiales, lo son. El punto sigue siendo que muestran los tipos de lugares donde escribir a = b = c es común, y en el caso del hardware, considerado una buena práctica, como lo solicitó el OP. Estoy seguro de que podrán considerar su relevancia para su entorno esperado
Michael Shaw

Mirando hacia atrás, mi problema con esta respuesta no es principalmente que se centre en casos de uso de programación del sistema (aunque eso sería lo suficientemente malo, la forma en que está escrito), sino que se basa en asumir una reescritura incorrecta. Los ejemplos no son ejemplos de lugares donde a=b=ces común / útil, son ejemplos de lugares donde se debe tener en cuenta el orden y la cantidad de efectos secundarios. Eso es completamente independiente. Vuelva a escribir la asignación encadenada correctamente y ambas variantes son igualmente correctas.

@delnan: el valor r se convierte al tipo de ben una temperatura, y eso se convierte al tipo de aen otra temperatura. El tiempo relativo de cuándo esos valores se almacenan realmente no está especificado. Desde una perspectiva de diseño de lenguaje, creo que es razonable exigir que todos los valores en una declaración de asignación múltiple tengan un tipo coincidente, y posiblemente exigir que ninguno de ellos sea volátil.
supercat

0

El mayor beneficio para mi mente de que la asignación sea una expresión es que permite que su gramática sea más simple si uno de sus objetivos es que "todo es una expresión", un objetivo de LISP en particular.

Python no tiene esto; tiene expresiones y declaraciones, la asignación es una declaración. Pero debido a que Python define una lambdaforma como una única expresión parametrizada , eso significa que no puede asignar variables dentro de una lambda. Esto es inconveniente a veces, pero no es un problema crítico, y es el único inconveniente en mi experiencia para que la asignación sea una declaración en Python.

Una forma de permitir que la asignación, o más bien el efecto de la asignación, sea una expresión sin introducir el potencial de if(x=1)accidentes que tiene C es usar una letconstrucción similar a LISP , como la (let ((x 2) (y 3)) (+ x y))que podría evaluar en su lenguaje 5. El uso de letesta manera no tiene por qué ser una tarea técnica en su idioma, si define letcomo crear un ámbito léxico. Definido de esa manera, una letconstrucción podría compilarse de la misma manera que construir y llamar a una función de cierre anidada con argumentos.

Por otro lado, si simplemente está interesado en el if(x=1)caso, pero desea que la asignación sea una expresión como en C, tal vez solo sea suficiente elegir diferentes tokens. Asignación: x := 1o x <- 1. Comparación: x == 1. Error de sintaxis: x = 1.


1
letdifiere de la asignación en más formas que técnicamente la introducción de una nueva variable en un nuevo alcance. Para empezar, no tiene ningún efecto sobre el código fuera del letcuerpo del 's, y por lo tanto requiere anidar todo el código (lo que debería usar la variable) más adelante, una desventaja significativa en el código de asignación pesada. Si uno fuera por esa ruta, set!sería el mejor análogo de Lisp, completamente diferente a la comparación, pero sin requerir anidamiento o un nuevo alcance.

@delnan: Me gustaría ver una combinación de sintaxis de declarar y asignar que prohibiría la reasignación pero permitiría la redeclaración, sujeto a las reglas que (1) la redeclaración solo sería legal para los identificadores de declarar y asignar, y (2 ) la redeclaración "anularía" una variable en todos los ámbitos de inclusión. Por lo tanto, el valor de cualquier identificador válido sería el asignado en la declaración anterior de ese nombre. Eso parecería un poco mejor que tener que agregar bloques de alcance para variables que solo se usan para unas pocas líneas, o tener que formular nuevos nombres para cada variable temporal.
supercat

0

Sé que esto significaría que esas funciones de una línea que las personas C aman tanto ya no funcionarían. Pensé (con poca evidencia más allá de mi experiencia personal) que la gran mayoría de las veces que esto sucedió, realmente tenía la intención de ser una operación de comparación.

En efecto. Esto no es nada nuevo, todos los subconjuntos seguros del lenguaje C ya han llegado a esta conclusión.

MISRA-C, CERT-C y así sucesivamente todas las asignaciones de prohibición dentro de las condiciones, simplemente porque es peligroso.

No existe ningún caso en el que el código que depende de la asignación dentro de las condiciones no pueda reescribirse.


Además, dichos estándares también advierten contra la escritura de código que se basa en el orden de evaluación. Asignaciones múltiples en una sola fila x=y=z;es tal caso. Si una fila con múltiples asignaciones contiene efectos secundarios (funciones de llamada, acceso a variables volátiles, etc.), no puede saber qué efecto secundario ocurrirá primero.

No hay puntos de secuencia entre la evaluación de los operandos. Por lo tanto, no podemos saber si la subexpresión yse evalúa antes o después z: es un comportamiento no especificado en C. Por lo tanto, dicho código es potencialmente poco confiable, no portátil y no conforme con los subconjuntos seguros mencionados de C.

La solución hubiera sido reemplazar el código con y=z; x=y;. Esto agrega un punto de secuencia y garantiza el orden de evaluación.


Entonces, en base a todos los problemas que esto causó en C, cualquier lenguaje moderno haría bien tanto en prohibir la asignación dentro de las condiciones, como en múltiples asignaciones en una sola fila.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.