Una característica que extraño de los lenguajes funcionales es la idea de que los operadores son solo funciones, por lo que agregar un operador personalizado suele ser tan simple como agregar una función. Muchos lenguajes de procedimiento permiten sobrecargas de operadores, por lo que, en cierto sentido, los operadores siguen siendo funciones (esto es muy cierto en D, donde el operador se pasa como una cadena en un parámetro de plantilla).
Parece que donde se permite la sobrecarga del operador, a menudo es trivial agregar operadores personalizados adicionales. Encontré esta publicación de blog , que argumenta que los operadores personalizados no funcionan bien con la notación infija debido a las reglas de precedencia, pero el autor da varias soluciones a este problema.
Miré a mi alrededor y no pude encontrar ningún lenguaje de procedimiento que admitiera operadores personalizados en el idioma. Hay hacks (como macros en C ++), pero eso no es lo mismo que el soporte de idiomas.
Dado que esta característica es bastante trivial de implementar, ¿por qué no es más común?
Entiendo que puede conducir a un código feo, pero eso no ha impedido que los diseñadores de idiomas agreguen características útiles que pueden ser fácilmente abusadas (macros, operador ternario, punteros inseguros).
Casos de uso reales:
- Implementar operadores faltantes (por ejemplo, Lua no tiene operadores bit a bit)
- Mimic D's
~
(concatenación de matriz) - DSL
- Úselo
|
como azúcar de sintaxis estilo tubería Unix (usando corutinas / generadores)
También estoy interesado en lenguajes que hacen que los operadores personalizados, pero estoy más interesado en por qué se ha excluido. Pensé en bifurcar un lenguaje de secuencias de comandos para agregar operadores definidos por el usuario, pero me detuve cuando me di cuenta de que no lo había visto en ningún lado, por lo que probablemente haya una buena razón por la que los diseñadores de idiomas más inteligentes que yo no lo han permitido.
fuente
Respuestas:
Hay dos escuelas de pensamiento diametralmente opuestas en el diseño del lenguaje de programación. Una es que los programadores escriben mejor código con menos restricciones, y la otra es que escriben mejor código con más restricciones. En mi opinión, la realidad es que los programadores experimentados prosperan con menos restricciones, pero esas restricciones pueden beneficiar la calidad del código de los principiantes.
Los operadores definidos por el usuario pueden crear un código muy elegante en manos experimentadas y un código completamente horrible para un principiante. Entonces, si su idioma los incluye o no, depende de la escuela de pensamiento de su diseñador de idiomas.
fuente
Format
método) y cuándo debe rechazar ( por ejemplo, argumentos de boxeo automático paraReferenceEquals
). Cuanto mayor sea el lenguaje de habilidad que los programadores puedan decir cuando ciertas inferencias serían inapropiadas, más seguro puede ofrecer inferencias convenientes cuando sea apropiado.Dada la opción entre concatenar matrices con ~ o con "myArray.Concat (secondArray)", probablemente preferiría la última. ¿Por qué? Porque ~ es un personaje completamente sin sentido que solo tiene su significado, el de concatenación de matriz, dado en el proyecto específico donde fue escrito.
Básicamente, como dijiste, los operadores no son diferentes de los métodos. Pero si bien los métodos pueden recibir nombres legibles y comprensibles que contribuyan a la comprensión del flujo de código, los operadores son opacos y situacionales.
Es por eso que tampoco me gusta el
.
operador de PHP (concatenación de cadenas) o la mayoría de los operadores en Haskell u OCaml, aunque en este caso, están surgiendo algunos estándares universalmente aceptados para lenguajes funcionales.fuente
+
y<<
desde luego no se definen enObject
(me sale "no puede competir con el operador + en ..." cuando se hace eso en una clase desnuda en C ++).Tu premisa está mal. Es no “bastante trivial para poner en práctica”. De hecho, trae una bolsa de problemas.
Echemos un vistazo a las "soluciones" sugeridas en la publicación:
Con todo, esta es una característica costosa de implementar, tanto en términos de complejidad del analizador como en términos de rendimiento, y no está claro que traiga muchos beneficios. Claro, hay algunos beneficios en la capacidad de definir nuevos operadores, pero incluso esos son polémicos (solo mire las otras respuestas argumentando que tener nuevos operadores no es algo bueno).
fuente
Ignoremos por el momento todo el argumento de "los operadores se abusan para perjudicar la legibilidad" y centrémonos en las implicaciones del diseño del lenguaje.
Los operadores Infix tienen más problemas que las simples reglas de precedencia (aunque para ser contundente, el enlace al que hace referencia trivializa el impacto de esa decisión de diseño). Una es la resolución de conflictos: ¿qué sucede cuando define
a.operator+(b)
yb.operator+(a)
? Preferir uno sobre el otro lleva a romper la propiedad conmutativa esperada de ese operador. Lanzar un error puede llevar a que los módulos que de otro modo funcionarían se rompan una vez juntos. ¿Qué sucede cuando comienzas a arrojar tipos derivados a la mezcla?El hecho es que los operadores no son solo funciones. Las funciones son independientes o pertenecen a su clase, lo que proporciona una preferencia clara sobre qué parámetro (si corresponde) posee el envío polimórfico.
Y eso ignora los diversos problemas de empaque y resolución que surgen de los operadores. La razón por la cual los diseñadores de idiomas (en general) limitan la definición del operador infijo es porque crea una pila de problemas para el idioma al tiempo que proporciona un beneficio discutible.
Y, francamente, porque son no triviales de implementar.
fuente
+
es malo. Pero, ¿es esto realmente un argumento en contra de los operadores definidos por el usuario? Parece un argumento en contra de la sobrecarga del operador en general.boost::spirit
. Tan pronto como permita que los operadores definidos por el usuario empeoren, ya que no hay una buena manera de definir bien la precedencia para las matemáticas. He escrito un poco sobre eso en el contexto de un lenguaje que busca específicamente abordar problemas con operadores definidos arbitrariamente.Creo que se sorprendería de la frecuencia con la que se implementan sobrecargas del operador de alguna forma. Pero no se usan comúnmente en muchas comunidades.
¿Por qué usar ~ para concatenar a una matriz? ¿Por qué no usar << como lo hace Ruby ? Debido a que los programadores con los que trabaja probablemente no sean programadores de Ruby. O los programadores D. Entonces, ¿qué hacen cuando se encuentran con su código? Tienen que ir a buscar lo que significa el símbolo.
Solía trabajar con un muy buen desarrollador de C # que también tenía gusto por los lenguajes funcionales. De la nada, comenzó a presentar mónadas a C # a través de métodos de extensión y utilizando la terminología estándar de mónada. Nadie podía discutir que parte de su código era más terser e incluso más legible una vez que se sabía lo que significaba, pero sí significaba que todos tenían que aprender terminología de mónada antes de que el código tuviera sentido .
Bastante justo, ¿crees? Era solo un pequeño equipo. Personalmente, no estoy de acuerdo. Cada nuevo desarrollador estaba destinado a confundirse con esta terminología. ¿No tenemos suficientes problemas para aprender un nuevo dominio?
Por otro lado, usaré con gusto el
??
operador en C # porque espero que otros desarrolladores de C # sepan qué es, pero no lo sobrecargaría en un lenguaje que no lo admite de forma predeterminada.fuente
??
ejemplo.Se me ocurren algunas razones:
O(1)
. Pero con la sobrecarga del operador,someobject[i]
fácilmente podría ser unaO(n)
operación dependiendo de la implementación del operador de indexación.En realidad, hay muy pocos casos en los que la sobrecarga del operador tenga usos justificables en comparación con el uso de funciones regulares. Un ejemplo legítimo podría ser el diseño de una clase de números complejos para uso de los matemáticos, que comprenden las formas bien entendidas en que los operadores matemáticos se definen para los números complejos. Pero esto realmente no es un caso muy común.
Algunos casos interesantes a considerar:
+
es solo una función regular. Puede definir las funciones como desee (normalmente hay una forma de definirlas en espacios de nombres separados para evitar conflictos con el incorporado+
), incluidos los operadores. Pero hay una tendencia cultural a usar nombres de funciones significativos, por lo que no se abusa mucho de esto. Además, en el prefijo de Lisp, la notación tiende a usarse exclusivamente, por lo que hay menos valor en el "azúcar sintáctico" que proporcionan las sobrecargas del operador.cout << "Hello World!"
¿alguien?), Pero el enfoque tiene sentido dado el posicionamiento de C ++ como un lenguaje complejo que permite la programación de alto nivel y al mismo tiempo le permite acercarse mucho al metal para el rendimiento, por lo que puede, por ejemplo, escribir una clase de números complejos que se comporte exactamente como lo desee sin comprometer el rendimiento. Se entiende que es su responsabilidad si se dispara en el pie.fuente
No es trivial de implementar (a menos que se implemente trivialmente). Tampoco te ayuda mucho, incluso si se implementa de manera ideal: las ganancias de legibilidad de la conmoción se compensan con las pérdidas de legibilidad por desconocimiento y opacidad. En resumen, es poco común porque no suele valer la pena el tiempo de los desarrolladores o los usuarios.
Dicho esto, puedo pensar en tres idiomas que lo hacen, y lo hacen de diferentes maneras:
fuente
Una de las principales razones por las que se desalienta a los operadores personalizados es porque cualquier operador puede querer / puede hacer cualquier cosa.
Por ejemplo
cstream
, la sobrecarga de desplazamiento a la izquierda muy criticada.Cuando un lenguaje permite sobrecargas del operador, generalmente existe un estímulo para mantener el comportamiento del operador similar al comportamiento base para evitar confusiones.
Además, los operadores definidos por el usuario hacen que el análisis sea mucho más difícil, especialmente cuando también hay reglas de preferencia personalizadas.
fuente
+
suma dos cosas, las-
resta, las*
multiplica. Mi sensación es que nadie obliga al programador a hacer que la función / métodoadd
realmente agregue algo ydoNothing
pueda lanzar armas nucleares. Ya.plus(b.minus(c.times(d)).times(e)
es mucho menos legible entoncesa + (b - c * d) * e
(bono adicional, donde en la primera picadura hay un error en la transcripción). No veo cómo primero es más significativo ...No usamos operadores definidos por el usuario por la misma razón que no usamos palabras definidas por el usuario. Nadie llamaría a su función "sworp". La única forma de transmitir su pensamiento a otra persona es usar un lenguaje compartido. Y eso significa que tanto las palabras como los signos (operadores) deben ser conocidos por la sociedad para la que está escribiendo su código.
Por lo tanto, los operadores que ve en uso en los lenguajes de programación son los que nos han enseñado en la escuela (aritmética) o los que se han establecido en la comunidad de programación, como por ejemplo los operadores booleanos.
fuente
elem
es una gran idea y ciertamente un operador que todos deberían entender, pero otros parecen estar en desacuerdo.En cuanto a los lenguajes que admiten tal sobrecarga: Scala sí, de hecho de una manera mucho más limpia y mejor puede C ++. La mayoría de los caracteres se pueden usar en los nombres de funciones, por lo que puede definir operadores como! + * = ++, si lo desea. Hay soporte incorporado para infijo (para todas las funciones que toman un argumento). Creo que también puedes definir la asociatividad de tales funciones. Sin embargo, no puedes definir la precedencia (solo con trucos feos, mira aquí ).
fuente
Una cosa que aún no se ha mencionado es el caso de Smalltalk, donde todo (incluidos los operadores) es un mensaje enviado. A los "operadores" les gusta
+
,|
etc., en realidad son métodos unarios.Todos los métodos pueden anularse, por lo que
a + b
significa suma de enteros sia
yb
son ambos enteros, y significa suma de vectores si ambos sonOrderedCollection
s.No hay reglas de precedencia, ya que estas son solo llamadas a métodos. Esto tiene una implicación importante para la notación matemática estándar:
3 + 4 * 5
medios(3 + 4) * 5
, no3 + (4 * 5)
.(Este es un gran obstáculo para los novatos de Smalltalk. Romper las reglas de las matemáticas elimina un caso especial, de modo que toda la evaluación del código se lleva a cabo de manera uniforme de izquierda a derecha, haciendo el lenguaje mucho más simple).
fuente
Estás luchando contra dos cosas aquí:
En la mayoría de los idiomas, los operadores no se implementan realmente como funciones simples. Es posible que tengan algunos andamios de funciones, pero el compilador / tiempo de ejecución es explícitamente consciente de su significado semántico y de cómo traducirlos de manera eficiente al código de la máquina. Esto es mucho más cierto incluso en comparación con las funciones integradas (por lo que la mayoría de las implementaciones tampoco incluyen todos los gastos generales de llamadas a funciones en su implementación). La mayoría de los operadores son abstracciones de nivel superior en las instrucciones primitivas que se encuentran en las CPU (que es en parte por qué la mayoría de los operadores son aritméticos, booleanos o bit a bit). Puede modelarlas como funciones "especiales" (llámelas "primitivas" o "integradas" o "nativas" o lo que sea), pero para hacer eso genéricamente se requiere un conjunto de semánticas muy robusto para definir tales funciones especiales. La alternativa es tener operadores integrados que se parezcan semánticamente a los operadores definidos por el usuario, pero que de otro modo invoquen rutas especiales en el compilador. Eso va en contra de la respuesta a la segunda pregunta ...
Aparte del problema de traducción automática que mencioné anteriormente, a nivel sintáctico, los operadores no son realmente diferentes de las funciones. Son características distintivas que tienden a ser concisas y simbólicas, lo que sugiere una característica adicional significativa que deben tener para ser útiles: deben tener un significado / semántica ampliamente comprensible para los desarrolladores. Los símbolos cortos no transmiten mucho significado a menos que sea una mano corta para un conjunto de semánticas que ya se entienden. Eso hace que los operadores definidos por el usuario sean inherentemente inútiles, ya que, por su propia naturaleza, no son tan ampliamente entendidos. Tienen tanto sentido como los nombres de funciones de una o dos letras.
Las sobrecargas del operador de C ++ proporcionan un terreno fértil para examinar esto. La mayoría de los "abusos" por sobrecarga del operador se presentan en forma de sobrecargas que rompen parte del contrato semántico que se entiende ampliamente (un ejemplo clásico es una sobrecarga del operador + tal que a + b! = B + a, o donde + modifica cualquiera de sus operandos).
Si observa Smalltalk, que permite la sobrecarga de operadores y los operadores definidos por el usuario, puede ver cómo un lenguaje podría hacerlo y qué tan útil sería. En Smalltalk, los operadores son simplemente métodos con diferentes propiedades sintácticas (es decir, están codificados como infinarios binarios). El lenguaje utiliza "métodos primitivos" para operadores y métodos acelerados especiales. Usted encuentra que pocos si se crean operadores definidos por el usuario, y cuando lo son, tienden a no usarse tanto como el autor probablemente pretendía que se usaran. Incluso el equivalente de una sobrecarga del operador es raro, porque es principalmente una pérdida neta definir una nueva función como operador en lugar de un método, ya que este último permite una expresión de la semántica de la función.
fuente
Siempre he encontrado que las sobrecargas de operadores en C ++ son un atajo conveniente para un equipo de un solo desarrollador, pero que causa todo tipo de confusión a largo plazo simplemente porque las llamadas al método están "ocultas" de una manera que no es fácil para que las herramientas como doxygen se separen, y las personas necesitan comprender los modismos para usarlos adecuadamente.
A veces es mucho más difícil de entender de lo que cabría esperar, incluso. Érase una vez, en un gran proyecto C ++ multiplataforma, decidí que sería una buena idea normalizar la forma en que se construían los caminos creando un
FilePath
objeto (similar alFile
objeto de Java ), que tendría operador / utilizado para concatenar otro parte de la ruta en él (para que pueda hacer algo comoFile::getHomeDir()/"foo"/"bar"
y haría lo correcto en todas nuestras plataformas compatibles). Todos los que lo vieron esencialmente dirían: "¿Qué demonios? ¿División de cuerdas? ... Oh, eso es lindo, pero no confío en que haga lo correcto".Del mismo modo, hay muchos casos en la programación de gráficos u otras áreas donde las matemáticas vectoriales / matriciales suceden muchas veces donde es tentador hacer cosas como Matrix * Matrix, Vector * Vector (punto), Vector% Vector (cross), Matrix * Vector ( transformación de matriz), matriz ^ Vector (transformación de matriz de caso especial que ignora la coordenada homogénea, útil para las normales de superficie), y así sucesivamente, pero si bien ahorra un poco de tiempo de análisis para la persona que escribió la biblioteca matemática de vectores, solo termina hasta confundir el tema más para otros. Simplemente no vale la pena.
fuente
Las sobrecargas de operadores son una mala idea por la misma razón que las sobrecargas de métodos son una mala idea: el mismo símbolo en la pantalla tendría diferentes significados dependiendo de lo que esté a su alrededor. Esto lo hace más difícil para la lectura casual.
Dado que la legibilidad es un aspecto crítico de la mantenibilidad, siempre debe evitar la sobrecarga (excepto en algunos casos muy especiales). Es mucho mejor que cada símbolo (ya sea operador o identificador alfanumérico) tenga un significado único que sea independiente.
Para ilustrar: cuando lee un código desconocido, si encuentra un nuevo identificador alfanumérico que no conoce, al menos tiene la ventaja de que sabe que no lo sabe. Entonces puedes ir a buscarlo. Sin embargo, si ve un identificador u operador común del que conoce el significado, es mucho menos probable que note que en realidad se ha sobrecargado para tener un significado completamente diferente. Para saber qué operadores se han sobrecargado (en una base de código que hizo un uso generalizado de la sobrecarga), necesitaría un conocimiento práctico del código completo, incluso si solo desea leer una pequeña parte de él. Esto dificultaría que los nuevos desarrolladores se pusieran al día con ese código, y sería imposible atraer a las personas para un trabajo pequeño. Esto puede ser bueno para la seguridad laboral del programador, pero si usted es responsable del éxito de la base del código, debe evitar esta práctica a toda costa.
Debido a que los operadores son de tamaño pequeño, los operadores de sobrecarga permitirían un código más denso, pero hacer que el código sea denso no es un beneficio real. Una línea con el doble de lógica tarda el doble en leerse. Al compilador no le importa. El único problema es la legibilidad humana. Dado que hacer que el código sea compacto no mejora la legibilidad, la compacidad no tiene ningún beneficio real. Siga adelante y tome el espacio, y dé a las operaciones únicas un identificador único, y su código tendrá más éxito a largo plazo.
fuente
Dificultades técnicas para manejar la precedencia y el análisis complejo, dejando de lado, creo que hay algunos aspectos de lo que es un lenguaje de programación que debe considerarse.
Los operadores son generalmente construcciones lógicas cortas que están bien definidas y documentadas en el lenguaje central (comparar, asignar ...). También suelen ser difíciles de entender sin documentación (comparar
a^b
con,xor(a,b)
por ejemplo). Hay un número bastante limitado de operadores que en realidad podrían tener sentido en la programación normal (>, <, =, + etc.).Mi idea es que es mejor atenerse a un conjunto de operadores bien definidos en un idioma, y luego permitir la sobrecarga de esos operadores (dada una recomendación suave de que los operadores deberían hacer lo mismo, pero con un tipo de datos personalizado).
Sus casos de uso de
~
y|
realmente serían posibles con una simple sobrecarga del operador (C #, C ++, etc.). DSL es un área de uso válida, pero probablemente una de las únicas áreas válidas (desde mi punto de vista). Sin embargo, creo que hay mejores herramientas para crear nuevos idiomas dentro. Ejecutar un verdadero lenguaje DSL dentro de otro idioma no es tan difícil usando cualquiera de esas herramientas de compilación-compilación. Lo mismo ocurre con el "argumento LUA extendido". Lo más probable es que un idioma se defina principalmente para resolver problemas de una manera específica, no para ser una base para sub-idiomas (existen excepciones).fuente
Otro factor es que no siempre es sencillo definir una operación con los operadores disponibles. Quiero decir, sí, para cualquier tipo de número, el operador '*' puede tener sentido, y generalmente se implementan en el lenguaje o en los módulos existentes. Pero en el caso de las clases complejas típicas que necesita definir (cosas como ShipingAddress, WindowManager, ObjectDimensions, PlayerCharacter, etc.) ese comportamiento no está claro ... ¿Qué significa sumar o restar un número a una Dirección? Multiplicar dos direcciones?
Claro, puede definir que agregar una cadena a una clase ShippingAddress significa una operación personalizada como "reemplazar la línea 1 en la dirección" (en lugar de la función "setLine1") y agregar un número es "reemplazar el código postal" (en lugar de "setZipCode") , pero el código no es muy legible y confuso. Por lo general, pensamos que los operadores se utilizan en tipos / clases básicos, ya que su comportamiento es intuitivo, claro y consistente (al menos una vez que esté familiarizado con el lenguaje). Piense en tipos como Integer, String, ComplexNumbers, etc.
Por lo tanto, incluso si la definición de operadores puede ser muy útil en algunos casos específicos, su implementación en el mundo real es bastante limitada, ya que el 99% de los casos en que eso será una clara victoria ya están implementados en el paquete de idioma básico.
fuente