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) x
VB: 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 def
o dec
o var
de declaración de variables, por qué no ! name
o @ name
o algo similar. Ciertamente acorta la declaración seguida de un enlace: en @x := 5
lugar de declare x = 5
o 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?
fuente
+
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.Respuestas:
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
fuente
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 = 5
lee como"declare x equals 5"
olet x = 5
puede leerse como"let x equal 5"
, lo cual es muy comprensible cuando se lee en voz alta, sin embargo, la lectura@x := 5
se 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.
fuente
@x := 5
sea difícil de entender, todavía lo leo en voz alta para mí mismoset x to be equal to 5
.:=
tiene un uso más amplio en notación matemática fuera de los lenguajes de programación. Además, declaraciones como sex = x + 1
vuelven un poco absurdas.:=
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.:=
se usaba en matemáticas. La única definición que encontré para él era "está definido para ser", por@x := 5
lo 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.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 * f
con+(+(*(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).
fuente
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 * B
significa? Depende completamente del contexto: ¿esA
un 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 sonand
,or
,xor
ynot
, 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.fuente
and
yor
todo 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.|
(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.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
foldRight
más legible que/:
.fuente
fold
con 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.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 * c
es más fácil de escribir y leer queC = 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.
fuente
Supongo que el ejemplo más extremo es APL, el siguiente programa es el "juego de la vida":
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.
fuente