¿Son más claros los operadores que las palabras clave o funciones? [cerrado]

14

Es un poco subjetivo, pero espero obtener una comprensión más clara de los factores que hacen que un operador sea claro de usar vs obtuso y difícil. He estado considerando los diseños de idiomas recientemente, y un tema que siempre vuelvo a plantear es cuándo hacer que una operación clave en el idioma sea un operador, y cuándo usar una palabra clave o función.

Haskell es algo notorio por esto, ya que los operadores personalizados son fáciles de crear y, a menudo, un nuevo tipo de datos vendrá empaquetado con varios operadores para su uso. La biblioteca Parsec, por ejemplo, viene con un montón de operadores para combinar analizadores, con gemas como >.y .> ni siquiera puedo recordar lo que significan en este momento, pero sí recuerdo que es muy fácil trabajar con ellos una vez que memoricé qué en realidad quieren decir. ¿Sería mejor una llamada a una función como leftCompose(parser1, parser2)? Ciertamente más detallado, pero más claro en algunos aspectos.

Las sobrecargas de operadores en lenguajes tipo C son un problema similar, pero se combinan con el problema adicional de sobrecargar el significado de operadores familiares como +con nuevos significados inusuales.

En cualquier idioma nuevo, esto parecería un problema bastante difícil. En F #, por ejemplo, la conversión utiliza un operador de conversión de tipo derivado matemáticamente, en lugar de la sintaxis de conversión de estilo C # o el estilo detallado de VB. C #: (int32) xVB: CType(x, int32)F #:x :> int32

En teoría, un nuevo lenguaje podría tener operadores para la mayoría de las funciones integradas. En lugar de defo deco varde declaración de variables, por qué no ! nameo @ nameo algo similar. Ciertamente acorta la declaración seguida de un enlace: en @x := 5lugar de declare x = 5o let x = 5 La mayoría del código requerirá muchas definiciones de variables, entonces ¿por qué no?

¿Cuándo es un operador claro y útil, y cuándo se está oscureciendo?

CodexArcanum
fuente
1
Desearía que uno de los 1,000 desarrolladores que alguna vez se hayan quejado conmigo sobre la falta de sobrecarga del operador de Java respondiera esta pregunta.
smp7d
1
Pensé en APL cuando terminé de escribir la pregunta, pero de alguna manera eso aclara el punto y lo mueve a un territorio algo menos subjetivo. Creo que la mayoría de las personas podrían estar de acuerdo en que APL pierde algo en la facilidad de uso a través de su número extremo de operadores, pero la cantidad de personas que se quejan cuando un idioma no tiene sobrecarga de operadores seguramente habla a favor de que algunas funciones sean adecuadas para Actuar como operadores. Entre operadores personalizados prohibidos y nada más que operadores, deben ocultarse algunas pautas para la implementación y el uso prácticos.
CodexArcanum
Tal como lo veo, la falta de sobrecarga del operador es objetable porque privilegia los tipos nativos sobre los tipos definidos por el usuario (tenga en cuenta que Java también lo hace de otras maneras). Soy mucho más ambivalente acerca de los operadores personalizados al estilo Haskell, que parecen una invitación abierta a los problemas ...
tormenta que se avecina el
2
@comingstorm: creo que la forma de Haskell es mejor. Cuando tiene un conjunto finito de operadores que puede sobrecargar de diferentes maneras, a menudo se ve obligado a reutilizar operadores en diferentes contextos (por ejemplo, +para concatenación de cadenas o <<para secuencias). Con Haskell, por otro lado, un operador es o bien simplemente una función con un nombre personalizado (es decir, no sobrecargado) o parte de una clase de tipo que significa que si bien es polimórfico, que hace lo mismo lógica cosa para cada tipo y incluso tiene el mismo tipo de firma. Así >>es >>para cada tipo y nunca va a ser un poco turno.
Tikhon Jelvis

Respuestas:

16

desde el punto de vista del diseño del lenguaje general, no es necesario que haya ninguna diferencia entre funciones y operadores. Se podrían describir funciones como operaciones de prefijo con cualquier aridad (o incluso variable). Y las palabras clave se pueden ver solo como funciones u operadores con nombres reservados (lo cual es útil para diseñar una gramática).

Todas estas decisiones eventualmente se reducen a cómo desea que se lea la notación y que, como usted dice, es subjetiva, aunque uno puede hacer algunas racionalizaciones obvias, por ejemplo, use los operadores infix habituales para las matemáticas como todos los conocen.

Por último, Dijkstra escribió una justificación interesante de las anotaciones matemáticas que utilizó, que incluye una buena discusión sobre las compensaciones de las anotaciones infix vs prefix

jk.
fuente
44
Me encantaría darle un segundo +1 para el enlace al documento de Dijkstra.
Programador
Gran enlace, ¡tenías que publicar una cuenta de PayPal! ;)
Adriano Repetti
8

Para mí, un operador deja de ser útil cuando ya no puede leer la línea de código en voz alta o en su cabeza, y tiene sentido.

Por ejemplo, declare x = 5lee como "declare x equals 5"o let x = 5puede leerse como "let x equal 5", lo cual es muy comprensible cuando se lee en voz alta, sin embargo, la lectura @x := 5se lee como "at x colon equals 5"(o "at x is defined to be 5"si eres un matemático), lo que no tiene ningún sentido.

Entonces, en mi opinión, use un operador si el código puede ser leído en voz alta y entendido por una persona razonablemente inteligente que no esté familiarizada con el lenguaje, pero use nombres de funciones si no.

Rachel
fuente
55
No creo que @x := 5sea ​​difícil de entender, todavía lo leo en voz alta para mí mismo set x to be equal to 5.
FrustratedWithFormsDesigner
3
@Rachel, en ese caso, :=tiene un uso más amplio en notación matemática fuera de los lenguajes de programación. Además, declaraciones como se x = x + 1vuelven un poco absurdas.
ccoakley
3
Irónicamente, había olvidado que algunas personas no reconocerían :=como tarea. Algunos idiomas realmente usan eso. Smalltalk, creo, es quizás el más conocido. Si la asignación y la igualdad deben ser operadores separados es una lata completamente diferente de gusanos, uno probablemente ya cubierto por otra pregunta.
CodexArcanum
1
@ccoakley Gracias, no sabía que :=se usaba en matemáticas. La única definición que encontré para él era "está definido para ser", por @x := 5lo que sería traducido al inglés "at x is defined to be 5", lo que para mí todavía no tiene tanto sentido como debería.
Rachel
1
Ciertamente, Pascal utilizó: = como asignación, por falta de una flecha izquierda en ASCII.
Vatine
4

Un operador es claro y útil cuando le es familiar. Y eso probablemente significa que los operadores de sobrecarga deben hacerse solo cuando el operador elegido está lo suficientemente cerca (como en forma, precedencia y asociatividad) como una práctica ya establecida.

Pero cavando un poco más, hay dos aspectos.

La sintaxis (infijo para operador en lugar de prefijo para sintaxis de llamada de función) y la denominación.

Para la sintaxis, hay otro caso en el que los infijos son lo suficientemente más claros que los prefijos que pueden garantizar el esfuerzo de familiarizarse: cuando las llamadas se encadenan y se anidan.

Compare a * b + c * d + e * fcon +(+(*(a, b), *(c, d)), *(e, f))o + + * a b * c d * e f(ambos se pueden analizar en la misma condición que la versión infija). El hecho de que +separe los términos en lugar de preceder a uno de ellos por un largo camino lo hace más legible a mis ojos (pero debe recordar las reglas de precedencia que no son necesarias para la sintaxis de prefijos). Si necesita combinar las cosas de esta manera, el beneficio a largo plazo valdrá la pena el costo de aprendizaje. Y mantiene esta ventaja incluso si nombra a sus operadores con letras en lugar de usar símbolos.

Con respecto al nombramiento de operadores, veo pocas ventajas en el uso de algo más que símbolos establecidos o nombre claro. Sí, pueden ser más cortos, pero son realmente crípticos y se olvidarán rápidamente si no tiene una buena razón para conocerlos.

Un tercer aspecto si toma un POV de diseño de lenguaje, es si el conjunto de operadores debe estar abierto o cerrado, puede agregar operador o no, y si la prioridad y la asociatividad deben ser especificables por el usuario o no. Mi primera opción sería tener cuidado y no proporcionar prioridad y asociatividad especificables por el usuario, mientras que sería más abierto a tener un conjunto abierto (aumentaría el impulso de usar la sobrecarga del operador, pero disminuiría el uso de uno malo) solo para beneficiarse de la sintaxis infija donde es útil).

Un programador
fuente
Si los usuarios pueden agregar operadores, ¿por qué no les permiten establecer la precedencia y la asociatividad de los operadores que han agregado?
Tikhon Jelvis
@TikhonJelvis, fue principalmente conservador, pero hay algunos aspectos a considerar si quieres poder usarlo para algo más que ejemplos. Por ejemplo, desea vincular la prioridad y la asociatividad al operador, no a un miembro dado del conjunto sobrecargado (elige el miembro después de que se realiza el análisis, la elección no puede influir en el análisis). Por lo tanto, para integrar bibliotecas utilizando el mismo operador, deben acordar su prioridad y asociatividad. Antes de agregar la posibilidad, trataría de descubrir por qué tan pocos idiomas seguían a Algol 68 (AFAIK none) y permitían definirlos.
Programador
Bueno, Haskell le permite definir operadores arbitrarios y establecer su precedencia y asociatividad. La diferencia es que no se pueden sobrecargar los operadores per se : se comportan como funciones normales. Esto significa que no puede tener diferentes bibliotecas tratando de usar el mismo operador para diferentes cosas. Como puede definir su propio operador, hay muchas menos razones para reutilizar los mismos para diferentes cosas.
Tikhon Jelvis
1

Los operadores como símbolos son útiles cuando intuitivamente tienen sentido. +y -son obviamente sumas y restas, por ejemplo, por lo que casi todos los idiomas los usan como operadores. Lo mismo pasa con la comparación ( <y >se les enseña en la escuela primaria, y <=y >=son extensiones intuitivas de las comparaciones básicas cuando se entiende que no hay ninguna tecla subrayada de comparación en el teclado estándar.)

*y /son menos obvio, pero son de uso universal y su posición en el teclado numérico justo al lado del +y -llaves ayuda a proporcionar contexto. C <<y >>están en la misma categoría: cuando entiendes lo que son un desplazamiento a la izquierda y un desplazamiento a la derecha, no es demasiado difícil entender las mnemotecnias detrás de las flechas.

Luego te encuentras con algunos de los verdaderamente extraños, cosas que pueden convertir el código C en una sopa de operador ilegible. (Escoger C aquí porque es un ejemplo muy pesado para el operador cuya sintaxis casi todos conocen, ya sea trabajando con C o con lenguajes descendientes). Por ejemplo, %operador. Todo el mundo sabe lo que es eso: es un signo de porcentaje ... ¿verdad? Excepto en C, no tiene nada que ver con la división por 100; Es el operador de módulo. Eso tiene cero sentido mnemónicamente, ¡pero ahí está!

Peor aún son los operadores booleanos. &como y tiene sentido, excepto que hay dos &operadores diferentes que hacen dos cosas diferentes, y ambos son sintácticamente válidos en cualquier caso en el que uno de ellos sea válido, aunque solo uno de ellos tenga sentido en cualquier caso. ¡Eso es solo una receta para la confusión! Los operadores o , xor y no son aún peores, ya que no usan símbolos mnemónicamente útiles, y tampoco son consistentes. (Sin operador de doble xor , y el lógico y el bit a bit no usan dos símbolos diferentes en lugar de un símbolo y una versión duplicada).

Y para empeorar las cosas, los operadores &y *se reutilizan para cosas completamente diferentes, lo que hace que el lenguaje sea más difícil de analizar tanto para las personas como para los compiladores. (¿Qué A * Bsignifica? Depende completamente del contexto: ¿es Aun tipo o una variable?)

Es cierto que nunca hablé con Dennis Ritchie y le pregunté sobre las decisiones de diseño que tomó en el lenguaje, pero gran parte de eso parece que la filosofía detrás de los operadores era "símbolos por el bien de los símbolos".

Compare esto con Pascal, que tiene los mismos operadores que C, pero una filosofía diferente para representarlos: los operadores deben ser intuitivos y fáciles de leer. Los operadores aritméticos son iguales. El operador de módulo es la palabra mod, porque no hay ningún símbolo en el teclado que obviamente significa "módulo". Los operadores lógicos son and, or, xory not, y hay solamente uno de cada uno; el compilador sabe si necesita la versión booleana o bit a bit en función de si está operando en booleanos o números, eliminando toda una clase de errores. Esto hace que el código Pascal sea mucho más fácil de entender que el código C, especialmente en un editor moderno con resaltado de sintaxis para que los operadores y las palabras clave sean visualmente distintos de los identificadores.

Mason Wheeler
fuente
44
Pascal no representa una filosofía diferente en absoluto. Si lees los papeles de Wirth, él usaba símbolos para cosas como andy ortodo el tiempo. Sin embargo, cuando desarrolló Pascal, lo hizo en un mainframe de Control Data, que usaba caracteres de 6 bits. Eso obligó a la decisión de usar palabras para muchas cosas, por la simple razón de que el conjunto de caracteres simplemente no tenía casi tanto espacio para símbolos y algo como ASCII. He usado tanto C como Pascal, y encuentro que C es mucho más legible. Puede preferir Pascal, pero eso es cuestión de gustos, no de hecho.
Jerry Coffin
Para agregar al comentario de @JerryCoffin sobre Wirth, sus idiomas posteriores (Modula, Oberon) están usando más símbolos.
Programador
@AProgrammer: Y nunca fueron a ninguna parte. ;)
Mason Wheeler
Pienso |(y, por extensión ||) para o tiene sentido. También lo ves todo el tiempo para alternar en otros contextos, como las gramáticas, y parece que está dividiendo dos opciones.
Tikhon Jelvis
1
Una ventaja de las palabras clave basadas en letras es que su espacio es mucho mayor que el de los símbolos. Por ejemplo, un lenguaje de estilo Pascal puede incluir operadores para "módulo" y "resto", y para la división de enteros frente a la de coma flotante (que tienen diferentes significados más allá de los tipos de sus operandos).
supercat
1

Creo que los operadores son mucho más legibles cuando su función refleja de cerca su propósito matemático y familiar. Por ejemplo, los operadores estándar como +, -, etc. son más legibles que Agregar o Restar al realizar sumas y restas. El comportamiento del operador debe estar claramente definido. Puedo tolerar una sobrecarga del significado de + para la concatenación de listas, por ejemplo. Idealmente, la operación no tendría efectos secundarios, devolviendo un valor en lugar de mutar.

Sin embargo, he tenido problemas con los operadores de acceso directo para funciones como fold. Obviamente, más experiencia con ellos facilitaría esto, pero me parece foldRightmás legible que /:.

Matt H
fuente
44
Tal vez la frecuencia de uso también entra en juego. No pensaría foldcon la suficiente frecuencia que un operador ahorra mucho tiempo. Esa también podría ser la razón por la que LISPy (+ 1 2 3) parece extraño pero (sumar 1 2 3) lo es menos. Tendemos a pensar en los operadores como estrictamente binarios. Los >>.operadores de estilo de Parsec pueden ser tontos, pero al menos lo principal que hace en Parsec es combinar analizadores sintácticos, por lo que los operadores pueden ser muy útiles.
CodexArcanum
1

El problema con los operadores es que solo hay una pequeña cantidad de ellos en comparación con la cantidad de nombres de métodos sensuales (!) Que podrían usarse en su lugar. Un efecto secundario es que los operadores definibles por el usuario tienden a introducir mucha sobrecarga.

Ciertamente, la línea C = A * x + b * ces más fácil de escribir y leer que C = A.multiplyVector(x).addVector(b.multiplyScalar(c)).

O, bueno, es ciertamente más fácil de escribir, si tienes todas las versiones sobrecargadas de todos esos operadores en tu cabeza. Y puede leerlo después, mientras arregla el penúltimo error.

Ahora, para la mayoría del código que se ejecutará, está bien. "El proyecto pasa todas las pruebas y se ejecuta", para muchos programas que es todo lo que podríamos desear.

Pero las cosas funcionan de manera diferente cuando haces un software que se dirige a áreas críticas. Cosas críticas de seguridad. Seguridad. Software que mantiene los aviones en el aire, o que mantiene en funcionamiento las instalaciones nucleares. O software que cifra sus correos electrónicos altamente confidenciales.

Para los expertos en seguridad que se especializan en el código de auditoría, esto puede ser una pesadilla. La combinación de una gran cantidad de operadores definidos por el usuario y la sobrecarga del operador puede dejarlos en la desagradable situación de que les resulta difícil averiguar qué código se ejecutará eventualmente.

Entonces, como se indicó en la pregunta original, este es un tema altamente subjetivo. Y aunque muchas sugerencias sobre cómo deberían usarse los operadores pueden sonar perfectamente sensuales, la combinación de solo unos pocos podría crear muchos problemas a largo plazo.

doppelfish
fuente
0

Supongo que el ejemplo más extremo es APL, el siguiente programa es el "juego de la vida":

life←{↑1 ⍵∨.∧3 4=+/,¯1 0 1∘.⊖¯1 0 1∘.⌽⊂⍵}

Y si puede averiguar lo que significa sin pasar un par de días con el manual de referencia, ¡buena suerte!

El problema es que tiene un conjunto de símbolos que casi todos entienden intuitivamente: +,-,*,/,%,=,==,&

y el resto, que requieren alguna explicación antes de que puedan entenderse, y, en general, son específicos del idioma en particular. Por ejemplo, no hay un símbolo obvio para especificar qué miembro de una matriz desea, [], () e incluso "." se han utilizado, pero tampoco existe una palabra clave obvia que pueda utilizar fácilmente, por lo que se debe defender el uso juicioso de los operadores. Pero no muchos de ellos.

James Anderson
fuente