¿Por qué los operadores definidos por el usuario no son más comunes?

92

Una característica que echo de menos 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 cuando se permite la sobrecarga de operadores, a menudo es trivial agregar operadores personalizados adicionales. Encontré esta publicación de blog , que sostiene que los operadores personalizados no funciona bien con la notación de infijo 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 idioma de procedimiento que admita operadores personalizados en el idioma. Hay hacks (como macros en C ++), pero eso no es lo mismo que el soporte de idioma.

Dado que esta función es bastante trivial de implementar, ¿por qué no es más común?

Entiendo que puede llevar a algún código feo, pero eso no ha impedido a los diseñadores de idiomas en el pasado agregar funciones útiles que se pueden abusar fácilmente (macros, operador ternario, punteros no seguros).

Casos de uso reales:

  • Implemente operadores que faltan (por ejemplo, Lua no tiene operadores bitwise)
  • ~ (concatenación de matriz) de Mimic D
  • DSL
  • Utilice | como azúcar de sintaxis de estilo de tubería de Unix (usando coroutines / generadores)

También me interesan los idiomas que permite permitir operadores personalizados, pero me interesa más por qué se ha excluido. Pensé en forzar un lenguaje de scripting para agregar operadores definidos por el usuario, pero me detuve cuando me di cuenta de que no lo había visto en ninguna parte, 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.

    
pregunta beatgammit 29.12.2012 - 19:23

16 respuestas

131

Hay dos escuelas de pensamiento diametralmente opuestas en el diseño del lenguaje de programación. Uno es que los programadores escriben mejor código con menos restricciones, y el otro es que escriben mejor código con más restricciones. En mi opinión, la realidad es que los buenos programadores experimentados florecen con menos restricciones, pero que las 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 absolutamente horrible para un principiante. Por lo tanto, si su idioma los incluye o no, depende de la escuela de pensamiento de su diseñador lingüístico.

    
respondido por el Karl Bielefeldt 29.12.2012 - 22:05
83

Dada la opción entre concatenar matrices con ~ o con "myArray.Concat (secondArray)", probablemente preferiría esta última. ¿Por qué? Porque ~ es un carácter completamente sin sentido que solo tiene su significado, el de concatenación de matrices, dado en el proyecto específico donde se escribió.

Básicamente, como dijiste, los operadores no son diferentes de los métodos. Pero mientras que a los métodos se les pueden dar nombres legibles y comprensibles que se agregan a la comprensión del flujo de código, los operadores son opacos y situacionales.

Esta es la razón por la que tampoco me gusta el operador . de PHP (concatenación de cadenas) o la mayoría de los operadores en Haskell o OCaml, aunque en este caso, están surgiendo algunos estándares universalmente aceptados para lenguajes funcionales.

    
respondido por el Avner Shahar-Kashtan 29.12.2012 - 19:31
70
  

Dado que esta función es bastante trivial de implementar, ¿por qué no es más común?

Su premisa es incorrecta. Es no "bastante trivial de implementar". De hecho, trae una bolsa de problemas.

Veamos las "soluciones" sugeridas en la publicación:

  • Sin prioridad . El propio autor dice: "No usar reglas de precedencia simplemente no es una opción".
  • Análisis consciente semántico . Como dice el artículo, esto requeriría que el compilador tenga mucho conocimiento semántico. El artículo no ofrece realmente una solución para esto y déjame decirte que esto simplemente no es trivial. Los compiladores están diseñados como una compensación entre poder y complejidad. En particular, el autor menciona un paso de análisis previo para recopilar la información relevante, pero el análisis previo es ineficiente y los compiladores se esfuerzan por minimizar los pases de análisis.
  • No hay operadores de infijo personalizados . Bueno, eso no es una solución.
  • Solución híbrida . Esta solución conlleva muchas (pero no todas) las desventajas del análisis semántico. En particular, dado que el compilador tiene que tratar tokens desconocidos como potencialmente representantes de operadores personalizados, a menudo no puede producir mensajes de error significativos. También puede requerir que la definición de dicho operador proceda con el análisis (para recopilar información de tipo, etc.), una vez más, lo que requiere un pase de análisis adicional.

En definitiva, 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 para 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).

    
respondido por el Konrad Rudolph 30.12.2012 - 13:38
25

Ignoremos por el momento el argumento completo de "los operadores son abusados para dañar la legibilidad", y concentrémonos en las implicaciones de diseño del lenguaje.

Los operadores de Infix tienen más problemas que las simples reglas de precedencia (aunque para ser contundentes, 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) y b.operator+(a) ? Preferir uno sobre el otro conduce 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 empiezas a lanzar tipos derivados en 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 clara preferencia sobre qué parámetro (si corresponde) posee el envío polimórfico.

Y eso ignora los diversos problemas de empaquetado y resolución que surgen de los operadores. La razón por la que los diseñadores de lenguajes (en general) limitan la definición del operador de infijo es porque crea una pila de problemas para el idioma a la vez que proporciona un beneficio discutible.

Y, francamente, porque no son triviales de implementar.

    
respondido por el Telastyn 30.12.2012 - 00:08
19

Creo que te sorprendería la frecuencia con la que sobrecargas de operadores se implementan en alguna forma. Pero no se usan comúnmente en muchas comunidades.

¿Por qué usar ~ para concatenar a una matriz? ¿Por qué no use < < como Ruby hace ? Porque los programadores con los que trabajas probablemente no son programadores de Ruby. O programadores D Entonces, ¿qué hacen cuando se encuentran con su código? Tienen que ir y buscar lo que significa el símbolo.

Solía trabajar con un muy buen desarrollador de C # que también tenía un gusto por los lenguajes funcionales. De la nada, comenzó a introducir mónadas a C # mediante métodos de extensión y utilizando la terminología de mónada estándar. Nadie pudo negar que parte de su código era terser e incluso más legible una vez que sabía lo que significaba, pero eso 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, con gusto utilizaré 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 admitiera de forma predeterminada.

    
respondido por el pdr 29.12.2012 - 21:25
11

Puedo pensar en algunas razones:

  • no son triviales de implementar : permitir operadores personalizados arbitrarios puede hacer que su compilador sea mucho más complejo, especialmente si permite reglas de prioridad, corrección y aridad definidas por el usuario. Si la simplicidad es una virtud, entonces la sobrecarga del operador lo está alejando del buen diseño del lenguaje.
  • son abusados , en su mayoría por programadores que piensan que es "genial" redefinir a los operadores y comenzar a redefinirlos para todo tipo de clases personalizadas. En poco tiempo, su código está lleno de una cantidad de símbolos personalizados que nadie más puede leer o entender porque los operadores no siguen las reglas convencionales bien entendidas. No compro el argumento "DSL", a menos que su DSL sea un subconjunto de las matemáticas :-)
  • perjudican la legibilidad y el mantenimiento : si los operadores se anulan regularmente, puede ser difícil detectar cuándo se está utilizando esta instalación, y los codificadores se ven obligados a preguntarse continuamente qué está haciendo el operador. Es mucho mejor dar nombres de función significativos. Escribir algunos caracteres adicionales es barato, los problemas de mantenimiento a largo plazo son costosos.
  • Pueden romper expectativas de rendimiento implícitas . Por ejemplo, normalmente esperaría que la búsqueda de un elemento en una matriz sea O(1) . Pero con la sobrecarga del operador, someobject[i] podría ser fácilmente una operación O(n) dependiendo de la implementación del operador de indexación.

En realidad, hay muy pocos casos en que la sobrecarga de operadores tenga usos justificables en comparación con el uso de funciones regulares. Un ejemplo legítimo podría ser diseñar una clase de números complejos para uso de matemáticos, que entiendan las formas bien entendidas en que se definen los operadores matemáticos para los números complejos. Pero esto realmente no es un caso muy común.

Algunos casos interesantes a considerar:

  • Lisps : en general, no se distingue entre operadores y funciones: + es solo una función regular. Puede definir las funciones como desee (normalmente, existe 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. Además, en Lisp, la notación de prefijo tiende a utilizarse exclusivamente, por lo que hay menos valor en el "azúcar sintáctica" que proporcionan las sobrecargas de los operadores.
  • Java : no permite la sobrecarga del operador. Esto es ocasionalmente molesto (para cosas como el caso del número complejo) pero, en promedio, es probablemente la decisión de diseño correcta para Java, que está pensada como un lenguaje simple de uso general para OOP. El código Java es en realidad bastante fácil de mantener para los desarrolladores con poca o mediana calificación como resultado de esta simplicidad.
  • C ++ tiene una sobrecarga de operadores muy sofisticada. A veces se abusa de esto ( cout << "Hello World!" ¿alguien?), Pero el enfoque tiene sentido dado que el posicionamiento de C ++ es un lenguaje complejo que permite la programación de alto nivel al mismo tiempo que le permite acercarse mucho al metal para un mejor rendimiento, por lo que puede, por ejemplo escriba una clase de número complejo que se comporte exactamente como usted quiere sin comprometer el rendimiento. Se entiende que es tu propia responsabilidad si te disparas en el pie.
respondido por el mikera 02.01.2013 - 06:17
8
  

Dado que esta función es bastante trivial de implementar, ¿por qué no es más común?

no es trivial de implementar (a menos que trivialmente implementado). Tampoco le da mucha importancia, incluso si se implementa idealmente: las ganancias de legibilidad de la tersidad se compensan con las pérdidas de legibilidad de falta de familiaridad y opacidad. En resumen, es poco frecuente porque, por lo general, no vale 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:

  • Racket, un esquema, cuando no es todo lo que S-expression-y permite y espera que escribas, lo que equivale a un analizador para cualquier sintaxis que desees extender (y proporciona enlaces útiles para que esto sea manejable).
  • Haskell, un lenguaje de programación puramente funcional, permite definir cualquier operador que consiste únicamente en puntuación, y le permite proporcionar un nivel de fijeza (10 disponibles) y una asociatividad. Los operadores ternarios, etc. se pueden crear a partir de operadores binarios y funciones de orden superior.
  • Agda, un lenguaje de programación de tipo dependiente, es extremadamente flexible con los operadores (documento aquí ) permitiendo que tanto if-then y if-then-else se definan como operadores en el mismo programa, pero como resultado, su lexer, analizador y evaluador están fuertemente acoplados.
respondido por el Alex R 30.12.2012 - 19:14
7

Una de las principales razones por las que se desalienta a los operadores personalizados es que, en cualquier caso, cualquier operador puede significar / puede hacer cualquier cosa.

Por ejemplo, la sobrecarga de turno de la izquierda muy criticada de cstream .

Cuando un lenguaje permite sobrecargas de operador, generalmente se recomienda mantener el comportamiento del operador similar al comportamiento básico para evitar confusiones.

También los operadores definidos por el usuario hacen que el análisis sea mucho más difícil, especialmente cuando también hay reglas de preferencias personalizadas.

    
respondido por el ratchet freak 29.12.2012 - 20:01
4

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 manera 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 hemos aprendido en la escuela (aritmética) o los que se han establecido en la comunidad de programación, como los operadores booleanos.

    
respondido por el Vagif Verdi 30.12.2012 - 19:32
4

En cuanto a los idiomas que soportan dicha sobrecarga: Scala sí, en realidad de una manera mucho más limpia y mejor puede C ++. La mayoría de los caracteres se pueden usar en 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 puede definir la prioridad (solo con trucos feos, consulte aquí ).

    
respondido por el kutschkem 23.05.2017 - 14:40
4

Una cosa que aún no se ha mencionado es el caso de Smalltalk, donde todo (incluidos los operadores) es un mensaje enviado. Los "operadores", como + , | , etc., en realidad son métodos únicos.

Todos los métodos se pueden anular, por lo que a + b significa la suma de enteros si a y b son enteros, y significa la suma de vectores si ambos son OrderedCollection 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 significa (3 + 4) * 5 , no 3 + (4 * 5) .

(Este es un obstáculo importante para los principiantes de Smalltalk. Romper las reglas matemáticas elimina un caso especial, de modo que toda la evaluación del código avanza uniformemente de izquierda a derecha, haciendo que el lenguaje sea mucho más simple).

    
respondido por el Frank Shearar 02.01.2013 - 08:53
3

Estás luchando contra dos cosas aquí:

  1. ¿Por qué los operadores existen en idiomas en primer lugar?
  2. ¿Cuál es la virtud de los operadores sobre las funciones / métodos?

En la mayoría de los idiomas, los operadores no se implementan realmente como funciones simples. Es posible que tengan alguna función de andamiaje, 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 (razón por la cual la mayoría de las implementaciones tampoco incluyen toda la sobrecarga de llamadas a funciones en su implementación). La mayoría de los operadores son abstracciones de nivel superior en instrucciones primitivas que se encuentran en las CPU (que es en parte la razón por la cual la mayoría de los operadores son aritméticos, booleanos o en modo de bits). Puede modelarlas como funciones "especiales" (llámelas "primitivas" o "incorporadas" o "nativas" o lo que sea), pero para hacerlo de manera genérica se requiere un conjunto muy sólido de semánticas para definir tales funciones especiales. La alternativa es tener operadores integrados que parezcan semánticamente como operadores definidos por el usuario, pero que de otra manera invocan 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 ser útiles: deben tener un significado / semántica ampliamente comprendido 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 intrínsecamente inútiles, ya que, por su propia naturaleza, no se entienden tan ampliamente. Tienen tanto sentido como los nombres de funciones de una o dos letras.

Las sobrecargas de los operadores de C ++ proporcionan un terreno fértil para examinar esto. El "abuso" de la mayoría de los operadores se presenta 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 a los operadores definidos por el usuario que sobrecargan a los operadores y , puede ver cómo podría hacerlo un idioma y cuán útil sería. En Smalltalk, los operadores son simplemente métodos con diferentes propiedades sintácticas (a saber, están codificados como infijos binarios). El lenguaje utiliza "métodos primitivos" para operadores y métodos acelerados especiales. Usted encuentra que se crean pocos operadores, si es que hay alguno, definidos por el usuario, y cuando lo son, tienden a no acostumbrarse tanto como el autor probablemente pretendía que fueran utilizados. Incluso el equivalente de una sobrecarga de operador es raro, porque es principalmente una pérdida neta definir una nueva función como un operador en lugar de un método, ya que este último permite una expresión de la semántica de la función.

    
respondido por el Christopher Smith 30.12.2012 - 21:14
1

Siempre he encontrado que las sobrecargas de operadores en C ++ son un atajo conveniente para un equipo de un solo desarrollador, pero que a largo plazo causa todo tipo de confusión simplemente porque las llamadas a los métodos se "ocultan" de una manera que no es No es fácil que las herramientas como el doxygen se desarmen, y la gente necesita entender los modismos para utilizarlos correctamente.

A veces es mucho más difícil de entender de lo que se podría esperar, incluso. Una vez, en un gran proyecto de C ++ multiplataforma decidí que sería una buena idea normalizar la forma en que se construyeron las rutas creando un objeto FilePath (similar al objeto File de Java), que tendría operador / utilizado para concatenar otra parte de la ruta en él (por lo que podría hacer algo como File::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".

De manera similar, hay muchos casos en la programación de gráficos u otras áreas donde las matemáticas de vector / matriz ocurren mucho donde es tentador hacer cosas como Matrix * Matrix, Vector * Vector (punto), Vector% Vector (cruz), Matrix * Vector (transformada de matriz), Matriz ^ Vector (transformada de matriz de caso especial que ignora la coordenada homogénea, útil para las normales de superficie), etc., pero a la vez que ahorra un poco de tiempo de análisis para la persona que escribió la biblioteca matemática de vectores, solo termina confundiendo más el tema para los demás. Simplemente no vale la pena.

    
respondido por el fluffy 26.05.2013 - 00:07
0

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é alrededor. Esto hace que sea más difícil para la lectura casual.

Dado que la legibilidad es un aspecto crítico de la capacidad de mantenimiento, 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 se mantenga por sí solo.

Para ilustrar: cuando lees un código desconocido, si encuentras un nuevo identificador alfanum que no conoces, al menos tienes la ventaja de que sabes que no lo sabes . Entonces puedes ir a buscarlo. Sin embargo, si ve un identificador u operador común del que sabe el significado, es mucho menos probable que note que en realidad se ha sobrecargado para que tenga 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á un conocimiento práctico del código completo, incluso si solo desea leer una pequeña parte del mismo. Esto dificultaría que los nuevos desarrolladores estén al tanto de ese código, e imposible atraer a las personas para un pequeño trabajo. Esto puede ser bueno para la seguridad del trabajo del programador, pero si usted es responsable del éxito del código base, debe evitar esta práctica a toda costa.

Debido a que los operadores son pequeños en tamañ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 lleva el doble de tiempo para leer. 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, no hay un beneficio real para la compacidad. Continúe y tome el espacio, y asigne a las operaciones únicas un identificador único, y su código tendrá más éxito a largo plazo.

    
respondido por el AgilePro 01.01.2013 - 03:47
-1

Las dificultades técnicas para manejar la precedencia y el análisis complejo, dejadas de lado, creo que hay algunos aspectos de lo que debe considerarse un lenguaje de programación.

Los operadores suelen ser 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 (por ejemplo, compare a^b con xor(a,b) ). Hay un número bastante limitado de operadores que realmente podrían tener sentido en la programación normal (>, & lt ;, =, + etc ..).

Mi idea es que es mejor ceñirse a un conjunto de operadores bien definidos en un idioma, y luego permitir que los operadores se sobrecarguen (dada una suave recomendación de que los operadores deberían hacer lo mismo, pero con un tipo de datos personalizado) .

Sus casos de uso de ~ y | serían realmente 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-compilador. Lo mismo ocurre con el "argumento LUA extendido". Es muy probable que un idioma se defina principalmente para resolver problemas de una manera específica, no para ser una base para sub-idiomas (existen excepciones).

    
respondido por el Petter Nordlander 30.12.2012 - 15:32
-1

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, por lo general, se implementa en el idioma 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 agregar o restar un número a una dirección? ¿Multiplica 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 entonces el código no es muy legible y confuso. Normalmente pensamos que los operadores se utilizan en tipos / clases básicas, ya que su comportamiento es intuitivo, claro y coherente (una vez que esté familiarizado con el idioma, al menos). Piense en tipos como Integer, String, ComplexNumbers, etc.

Por lo tanto, incluso si definir 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 los que será una clara victoria ya están implementados en el paquete de idioma básico.

    
respondido por el Khelben 06.01.2013 - 22:19

Lea otras preguntas en las etiquetas