¿El método está sobrecargando algo más que el azúcar sintáctico? [cerrado]

19

¿La sobrecarga de métodos es un tipo de polimorfismo? A mí me parece simplemente la diferenciación de métodos con el mismo nombre y diferentes parámetros. Entonces, stuff(Thing t)y stuff(Thing t, int n)son métodos completamente diferentes en lo que respecta al compilador y el tiempo de ejecución.

Crea la ilusión, por parte de la persona que llama, de que es el mismo método que actúa de manera diferente en diferentes tipos de objetos: el polimorfismo. Pero eso es solo una ilusión, porque en realidad stuff(Thing t)y stuff(Thing t, int n)son métodos completamente diferentes.

¿El método está sobrecargando algo más que el azúcar sintáctico? ¿Me estoy perdiendo de algo?


Una definición común para el azúcar sintáctico es que es puramente local . Lo que significa que cambiar un fragmento de código a su equivalente 'endulzado', o viceversa, implica cambios locales que no afectan la estructura general del programa. Y creo que la sobrecarga de métodos se ajusta precisamente a este criterio. Veamos un ejemplo para demostrar:

Considera una clase:

class Reader {
    public String read(Book b){
        // .. translate the book to text
    }
    public String read(File b){
        // .. translate the file to text
    }
}

Ahora considere otra clase que usa esta clase:

/* might not be the best example */
class FileProcessor {
    Reader reader = new Reader();
    public void process(File file){
        String text = reader.read(file);
        // .. do stuff with the text
    }
}

Bueno. Ahora veamos qué necesita cambiar si reemplazamos la sobrecarga de métodos con métodos regulares:

Los readmétodos en Readercambio a readBook(Book)y readFile(file). Solo es cuestión de cambiar sus nombres.

El código de llamada FileProcessorcambia ligeramente: reader.read(file)cambia a reader.readFile(file).

Y eso es.

Como puede ver, la diferencia entre usar la sobrecarga de métodos y no usarla es puramente local . Y es por eso que creo que califica como azúcar sintáctico puro.

Me gustaría escuchar sus objeciones si tiene alguna, tal vez me estoy perdiendo algo.

Aviv Cohn
fuente
48
Al final, cualquier característica del lenguaje de programación es solo azúcar sintáctico para ensamblador en bruto.
Philipp
31
@Philipp: Lo siento, pero esa es una declaración realmente estúpida. Los lenguajes de programación derivan su utilidad de la semántica, no de la sintaxis. Las características como un sistema de tipos le brindan garantías reales, aunque en realidad pueden requerir que escriba más .
back2dos
3
Pregúntese esto: ¿la sobrecarga del operador es solo azúcar sintáctico? Cualquier respuesta a esa pregunta que consideres verdadera también es la respuesta a la pregunta que hiciste;)
back2dos
55
@ back2dos: totalmente de acuerdo con usted. Leí la frase "todo es azúcar sintáctico para el ensamblador" con demasiada frecuencia, y está claramente equivocado. El azúcar sintáctico es una sintaxis alternativa (posiblemente mejor) para algunas sintaxis existentes que no agrega ninguna semántica nueva.
Giorgio
66
@Giorgio: correcto! Hay una definición precisa en el artículo histórico de Matthias Felleisen sobre expresividad. Básicamente: el azúcar sintáctico es puramente local. Si tiene que cambiar la estructura global del programa para eliminar el uso de la función de idioma, entonces no es azúcar sintáctica. Es decir, reescribir el código OO polimórfico en el ensamblador generalmente implica agregar una lógica de despacho global, que no es puramente local, por lo tanto, OO no es "solo azúcar sintáctico para el ensamblador".
Jörg W Mittag

Respuestas:

29

Para responder a esto, primero necesita una definición de "azúcar sintáctico". Iré con Wikipedia :

En informática, el azúcar sintáctico es la sintaxis dentro de un lenguaje de programación que está diseñado para facilitar la lectura o la expresión. Hace que el lenguaje sea "más dulce" para el uso humano: las cosas se pueden expresar de manera más clara, más concisa o en un estilo alternativo que algunos prefieran.

[...]

Específicamente, una construcción en un idioma se denomina azúcar sintáctico si se puede eliminar del idioma sin ningún efecto sobre lo que el idioma puede hacer.

Entonces, bajo esta definición, características como los varargs de Java o la comprensión de Scala son azúcar sintáctica: se traducen en características subyacentes del lenguaje (una matriz en el primer caso, llamadas a map / flatmap / filter en el segundo), y eliminarlas sería No cambie las cosas que puede hacer con el idioma.

La sobrecarga de métodos, sin embargo, no es azúcar sintáctica bajo esta definición, porque eliminarla cambiaría fundamentalmente el idioma (ya no sería capaz de enviar un comportamiento distinto basado en argumentos).

Es cierto que puede simular la sobrecarga de métodos siempre que tenga alguna forma de acceder a los argumentos de un método, y puede usar una construcción "if" basada en los argumentos que se le dan. Pero si considera ese azúcar sintáctico, debería considerar cualquier cosa por encima de una máquina de Turing para que también sea azúcar sintáctico.

kdgregory
fuente
22
Eliminar la sobrecarga no cambiaría lo que el idioma puede hacer. Todavía podrías hacer exactamente las mismas cosas que antes; solo tendría que cambiar el nombre de algunos métodos. Ese es un cambio más trivial que desgarrar bucles.
Doval
99
Como dije, podría adoptar el enfoque de que todos los idiomas (incluidos los lenguajes de máquina) son simplemente azúcar sintáctica encima de una máquina de Turing.
kdgregory
99
Como lo veo, la sobrecarga de métodos simplemente te permite hacer sum(numbersArray)y en sum(numbersList)lugar de sumArray(numbersArray)y sumList(numbersList). Estoy de acuerdo con Doval, parece un mero azúcar sintético.
Aviv Cohn
3
La mayor parte del lenguaje. Tratar implementar instanceof, clases, herencia, las interfaces, los genéricos, reflexión o especificadores de acceso que utilicen if, whiley los operadores booleanos, con exactamente la misma semántica . No hay casos de esquina. Tenga en cuenta que no le estoy desafiando a que calcule las mismas cosas que los usos específicos de esas construcciones. Ya que puedes calcular cualquier cosa usando lógica booleana y ramificación / bucle. Te estoy pidiendo para implementar copias perfectas de la semántica de las características del lenguaje, incluidas las garantías estáticos que proporcionan (comprobaciones en tiempo de compilación todavía deben hacerse en tiempo de compilación.)
Doval
66
@Doval, kdgregory: para definir el azúcar sintáctico, debe definirlo en relación con algunas semánticas. Si la única semántica que tiene es "¿Qué calcula este programa?", Entonces está claro que todo es azúcar sintáctica para una máquina Turing. Por otro lado, si tiene una semántica en la que puede hablar sobre objetos y ciertas operaciones sobre ellos, entonces eliminar cierta sintaxis ya no le permitirá expresar esas operaciones, aunque el lenguaje aún pueda ser Turing completo.
Giorgio
13

El término azúcar sintáctico generalmente se refiere a casos en los que la característica se define mediante una sustitución. El lenguaje no define lo que hace una característica, sino que define que es exactamente equivalente a otra cosa. Entonces, por ejemplo, bucles for-each

for(Object alpha: alphas) {
}

Se convierte en:

for(Iterator<Object> iter = alpha.iterator(); iter.hasNext()) {
   alpha = iter.next();
}

O tome una función con argumentos variables:

void foo(int... args);

foo(3, 4, 5);

Que se convierte en:

void Foo(int[] args);

foo(new int[]{3, 4, 5});

Por lo tanto, hay una sustitución trivial de la sintaxis para implementar la característica en términos de otras características.

Veamos la sobrecarga de métodos.

void foo(int a);
void foo(double b);

foo(4.5);

Esto puede reescribirse como:

void foo_int(int a);
void foo_double(double b);

foo_double(4.5);

Pero no es equivalente a eso. Dentro del modelo de Java, esto es algo diferente. foo(int a)no implementa una foo_intfunción para ser creada. Java no implementa la sobrecarga de métodos al dar nombres divertidos a funciones ambiguas. Para contar como azúcar sintáctico, Java tendría que fingir que realmente escribió foo_inty foo_doublefunciona, pero no es así.

Winston Ewert
fuente
2
No creo que nadie haya dicho que la transformación del azúcar de sintaxis sea trivial. Incluso si así fuera, considero que la afirmación es But, the transformation isn't trivial. At the least, you have to determine the types of the parameters.muy incompleta porque no es necesario determinar los tipos ; son conocidos en tiempo de compilación.
Doval
3
"Para contar como azúcar sintáctico, Java tendría que pretender que realmente escribiste funciones foo_int y foo_double, pero no es así". - Siempre y cuando hablemos de métodos de sobrecarga y no de polimorfismos, ¿cuál sería la diferencia entre foo(int)/ foo(double)y foo_int/ foo_double? Realmente no conozco muy bien Java, pero me imagino que tal cambio de nombre realmente ocurre en JVM (bueno, probablemente usando en foo(args)lugar defoo_args ) al menos en C ++ con símbolo de manipulación (ok, el símbolo de manipulación es técnicamente un detalle de implementación y no parte del lenguaje)
Maciej Piechotka
2
@Doval: "No creo que nadie haya dicho que la transformación del azúcar de sintaxis sea trivial". - Cierto, pero tiene que ser local . La única definición útil de azúcar sintáctica que conozco es del famoso artículo de Matthias Felleisen sobre expresividad lingüística, y básicamente dice que si puedes reescribir un programa escrito en lenguaje L + y (es decir, algún idioma L con alguna característica y ) en lenguaje L (es decir, un subconjunto de ese idioma sin función y ) sin cambiar la estructura global del programa (es decir, solo hacer cambios locales), entonces y es azúcar sintáctico en L + y y hace
Jörg W Mittag
2
... no aumenta la expresividad de L. Sin embargo, si usted no puede hacer eso, es decir, si hay que hacer cambios en la estructura global de su programa, entonces es que no azúcar sintáctico y lo hace , de hecho, marca L + y más expresivos que L . Por ejemplo, Java con forbucle mejorado no es más expresivo que Java sin él. (Es más agradable, más conciso, más legible y, en general, mejor, diría, pero no más expresivo). Sin embargo, no estoy seguro sobre el caso de sobrecarga. Probablemente tendré que volver a leer el periódico para estar seguro. Mi instinto dice que es azúcar sintáctico, pero no estoy seguro.
Jörg W Mittag
2
@MaciejPiechotka, si fuera parte de la definición del lenguaje que las funciones cambiaran de nombre, y pudieras acceder a la función con esos nombres, creo que sería azúcar sintáctico. Pero debido a que está oculto como un detalle de implementación, creo que eso lo descalifica de ser azúcar sintáctico.
Winston Ewert
8

Dado que el cambio de nombre funciona, ¿no tiene que ser nada más que azúcar sintáctica?

Le permite a la persona que llama imaginar que está llamando a la misma función, cuando no lo está. Pero podía saber los nombres reales de todas sus funciones. Solo si fuera posible lograr un polimorfismo retrasado al pasar una variable sin tipo a una función con tipo y establecer su tipo para que la llamada pueda ir a la versión correcta de acuerdo con el nombre, esta sería una característica verdadera del lenguaje.

Desafortunadamente, nunca he visto un idioma hacer esto. Cuando hay ambigüedad, estos compiladores no lo resuelven, insisten en que el escritor lo resuelva por ellos.

Jon Jay Obermark
fuente
La función que está buscando allí se llama "Despacho múltiple". Muchos idiomas lo admiten, incluidos Haskell, Scala y (desde 4.0) C #.
Iain Galloway
Me gustaría separar los parámetros en las clases de la sobrecarga de métodos directos. En el caso de sobrecarga de método directo, el programador escribe todas las versiones, el compilador solo sabe cómo elegir una. Eso es solo azúcar sintáctico, y se resuelve con un simple cambio de nombre, incluso para el envío múltiple. --- En presencia de parámetros en las clases, el compilador genera el código según sea necesario, y eso lo cambia por completo.
Jon Jay Obermark
2
Creo que lo malinterpretas. Por ejemplo, en C #, si uno de los parámetros a un método es dynamicentonces resolución de sobrecarga se produce en tiempo de ejecución, no en tiempo de compilación . Eso es el despacho múltiple, y no se puede replicar cambiando el nombre de las funciones.
Iain Galloway
Bastante fresco. Sin embargo, todavía puedo probar el tipo de variable, por lo que esta es solo una función incorporada superpuesta en azúcar sintáctico. Es una característica del lenguaje, pero apenas.
Jon Jay Obermark
7

Dependiendo del idioma, es azúcar sintáctico o no.

En C ++, por ejemplo, puede hacer cosas usando sobrecarga y plantillas que no serían posibles sin complicaciones (escriba manualmente todas las instancias de la plantilla o agregue muchos parámetros de plantilla).

Tenga en cuenta que el despacho dinámico es una forma de sobrecarga, resuelta dinámicamente en algunos parámetros (para algunos idiomas solo uno especial, esto , pero no todos los idiomas son tan limitados), y no llamaría a esa forma de sobrecarga de azúcar sintáctico.

Un programador
fuente
No estoy seguro de cómo las otras respuestas funcionan mucho mejor cuando son esencialmente incorrectas.
Telastyn
5

Para los idiomas contemporáneos, es solo azúcar sintáctico; de una manera completamente independiente del lenguaje, es más que eso.

Anteriormente, esta respuesta decía simplemente que es más que azúcar sintáctica, pero si lo verá en los comentarios, Falco planteó el punto de que había una pieza del rompecabezas que los lenguajes contemporáneos parecen faltar; no mezclan la sobrecarga de métodos con la determinación dinámica de qué función llamar en el mismo paso. Esto se aclarará más adelante.

He aquí por qué debería ser más.

Considere un lenguaje que admita sobrecarga de métodos y variables sin tipo. Podría tener los siguientes prototipos de métodos:

bool someFunction(int arg);

bool someFunction(string arg);

En algunos idiomas, es probable que se resigne a saber en tiempo de compilación a cuál de estos se llamaría mediante una línea de código determinada. Pero en algunos idiomas, no todas las variables se escriben (o todas se escriben implícitamente como Objecto lo que sea), así que imagine construir un diccionario cuyas claves correspondan con valores de diferentes tipos:

dict roomNumber; // some hotels use numbers, some use letters, and some use
                 // alphanumerical strings.  In some languages, built-in dictionary
                 // types automatically use untyped values for their keys to map to,
                 // so it makes more sense then to allow for both ints and strings in
                 // your code.

Ahora bien, ¿qué pasaría si quisieras postular someFunctiona uno de esos números de habitación? A esto le llamas:

someFunction(roomNumber[someSortOfKey]);

¿Se someFunction(int)llama o se someFunction(string)llama? Aquí puede ver un ejemplo en el que estos no son métodos totalmente ortogonales, especialmente en lenguajes de nivel superior. El lenguaje tiene que descubrir, durante el tiempo de ejecución, a cuál de estos llamar, por lo que aún debe considerar que estos son al menos el mismo método.

¿Por qué no simplemente usar plantillas? ¿Por qué no simplemente usar un argumento sin tipo?

Flexibilidad y control de grano más fino. A veces, usar plantillas / argumentos sin tipo es un mejor enfoque, pero a veces no lo son.

Debe pensar en casos en los que, por ejemplo, podría tener dos firmas de métodos que toman un inty un stringcomo argumentos, pero donde el orden es diferente en cada firma. Es muy posible que tenga una buena razón para hacerlo, ya que la implementación de cada firma puede hacer en gran medida lo mismo, pero con un giro ligeramente diferente; el registro podría ser diferente, por ejemplo. O incluso si hacen exactamente lo mismo, es posible que pueda recopilar automáticamente cierta información solo en el orden en que se especificaron los argumentos. Técnicamente, podría usar declaraciones de seudoconmutador para determinar el tipo de cada uno de los argumentos pasados, pero eso se vuelve desordenado.

Entonces, ¿este siguiente ejemplo es una mala práctica de programación?

bool stringIsTrue(int arg)
{
    if (arg.toString() == "0")
    {
        return false;
    }
    else
    {
        return true;
    }
}

bool stringIsTrue(Object arg)
{
    if (arg.toString() == "0")
    {
        return false;
    }
    else
    {
        return true;
    }
}

bool stringIsTrue(string arg)
{
    if (arg == "0")
    {
        return false;
    }
    else
    {
        return true;
    }
}

Sí, en general. En este ejemplo particular, podría evitar que alguien intente aplicar esto a ciertos tipos primitivos y recupere comportamientos inesperados (lo que podría ser algo bueno); pero supongamos que abrevié el código anterior y que, de hecho, usted tiene sobrecargas para todos los tipos primitivos, así como para Objects. Entonces este siguiente fragmento de código es realmente más apropiado:

bool stringIsTrue(untyped arg)
{
    if (arg.toString() == "0")
    {
        return false;
    }
    else
    {
        return true;
    }
}

Pero, ¿qué sucede si solo necesita usar esto para intsy strings, y qué pasa si desea que se vuelva verdadero en función de condiciones más simples o más complicadas en consecuencia? Entonces tiene una buena razón para usar la sobrecarga:

bool appearsToBeFirstFloor(int arg)
{
    if (arg.digitAt(0) == 1)
    {
        return true;
    }
    else
    {
        return false;
    }
}

bool appearsToBeFirstFloor(string arg)
{
    string firstCharacter = arg.characterAt(0);
    if (firstCharacter.isDigit())
    {
        return appearsToBeFirstFloor(int(firstCharacter));
    }
    else if (firstCharacter.toUpper() == "A")
    {
        return true;
    }
    else
    {
        return false;
    }
}

Pero oye, ¿por qué no solo dar a esas funciones dos nombres diferentes? Todavía tienes la misma cantidad de control de grano fino, ¿no?

Porque, como se indicó anteriormente, algunos hoteles usan números, algunos usan letras y otros usan una combinación de números y letras:

appearsToBeFirstFloor(roomNumber[someSortOfKey]);

// will treat ints and strings differently, without you having to write extra code
// every single spot where the function is being called

Este todavía no es exactamente el mismo código exacto que usaría en la vida real, pero debería ilustrar el punto que estoy haciendo bien.

Pero ... He aquí por qué no es más que azúcar sintáctica en los idiomas contemporáneos.

Falco planteó el punto en los comentarios de que los lenguajes actuales básicamente no mezclan la sobrecarga de métodos y la selección dinámica de funciones dentro del mismo paso. La forma en que anteriormente entendía que ciertos idiomas funcionaban era que se podía sobrecargarappearsToBeFirstFloor en el ejemplo anterior, y luego el idioma determinaría en tiempo de ejecución qué versión de la función se llamará, dependiendo del valor de tiempo de ejecución de la variable sin tipo. Esta confusión se debió en parte al trabajar con lenguajes de tipo ECMA, como ActionScript 3.0, en el que puede aleatorizar fácilmente qué función se llama en una determinada línea de código en tiempo de ejecución.

Como ya sabrás, ActionScript 3 no admite la sobrecarga de métodos. En cuanto a VB.NET, puede declarar y establecer variables sin asignar un tipo explícitamente, pero cuando intenta pasar estas variables como argumentos a métodos sobrecargados, aún no desea leer el valor de tiempo de ejecución para determinar qué método llamar; en su lugar, quiere encontrar un método con argumentos de tipo Objecto sin tipo o algo por el estilo. Por lo que el intvs. stringejemplo anterior no funcionaría en ese idioma tampoco. C ++ tiene problemas similares, ya que cuando usa algo como un puntero nulo o algún otro mecanismo como ese, aún requiere que desambigue manualmente el tipo en tiempo de compilación.

Entonces, como dice el primer encabezado ...

Para los idiomas contemporáneos, es solo azúcar sintáctico; de una manera completamente independiente del lenguaje, es más que eso. Hacer que la sobrecarga de métodos sea más útil y relevante, como en el ejemplo anterior, en realidad puede ser una buena característica para agregar a un lenguaje existente (como se ha solicitado ampliamente implícitamente para AS3), o también podría servir como uno entre muchos pilares fundamentales diferentes para la creación de un nuevo lenguaje procesal / orientado a objetos.

Panzercrisis
fuente
3
¿Puedes nombrar algún idioma que realmente maneje Function-Dispatch en tiempo de ejecución y no en tiempo de compilación? TODOS los idiomas que conozco requieren certeza en tiempo de compilación de qué función se llama ...
Falco
@Falco ActionScript 3.0 lo maneja en tiempo de ejecución. Se podría, por ejemplo, utilizar una función que devuelve una de las tres cadenas al azar, y luego usar su valor de retorno para llamar a cualquiera de las tres funciones al azar: this[chooseFunctionNameAtRandom](); Si chooseFunctionNameAtRandom()devuelve o "punch", "kick"o "dodge", a continuación, puede aplicar thusly una manera muy simple al azar elemento en, por ejemplo, la IA de un enemigo en un juego Flash.
Panzercrisis
1
Sí, pero ambos son métodos semánticos reales para obtener el despacho dinámico de funciones, Java también los tiene. Pero son diferentes de la sobrecarga, la sobrecarga es estática y solo azúcar sintáctica, mientras que el despacho dinámico y la herencia son características de lenguaje real, que ofrecen una nueva funcionalidad.
Falco
1
... También probé los punteros nulos en C ++, así como los punteros de clase base, pero el compilador quería que lo desambiguara yo mismo antes de pasarlo a una función. Así que ahora me pregunto si eliminar esta respuesta. Está empezando a parecer que los idiomas casi siempre se combinan para combinar la elección dinámica de funciones con la sobrecarga de funciones en la misma instrucción o declaración, pero luego se alejan en el último segundo. Sin embargo, sería una buena característica del lenguaje; tal vez alguien necesita hacer un lenguaje que tenga esto.
Panzercrisis
1
Deje que la respuesta se quede, ¿tal vez piense en incluir parte de su investigación de los comentarios en la respuesta?
Falco
2

Realmente depende de su definición de "azúcar sintáctico". Trataré de abordar algunas de las definiciones que se me ocurren:

  1. Una característica es el azúcar sintáctico cuando un programa que lo utiliza siempre se puede traducir en otro que no la utiliza.

    Aquí suponemos que existe un conjunto primitivo de características que no se pueden traducir: en otras palabras, no hay bucles del tipo "puede reemplazar la característica X usando la característica Y" y "puede reemplazar la característica Y con la característica X". Si uno de los dos es verdadero, la otra característica se puede expresar en términos de características que no son la primera o es una característica primitiva.

  2. Igual que la definición 1, pero con el requisito adicional de que el programa traducido sea tan seguro para los tipos como el primero, es decir, al desaguar no pierde ningún tipo de información.

  3. La definición del OP: una característica es el azúcar sintáctico si su traducción no cambia la estructura del programa, sino que solo requiere "cambios locales".

Tomemos a Haskell como ejemplo de sobrecarga. Haskell proporciona sobrecarga definida por el usuario a través de clases de tipo. Por ejemplo, las operaciones +y *se definen en la Numclase de tipo y se puede usar cualquier tipo que tenga una instancia (completa) de dicha clase +. Por ejemplo:

instance Num a => Num (b, a) where
    (x, y) + (_, y') = (x, y + y')
    -- other definitions

("Hello", 1) + ("World", 3) -- -> ("Hello", 4)

Una cosa bien conocida sobre las clases de tipos de Haskell es que puedes deshacerte de ellas . Es decir, puede traducir cualquier programa que use clases de tipos en un programa equivalente que no las use.

La traducción es bastante simple:

  • Dada una definición de clase:

    class (P_1 a, ..., P_n a) => X a where
        op_1 :: t_1   ... op_m :: t_m
    

    Puede traducirlo a un tipo de datos algebraicos:

    data X a = X {
        X_P_1 :: P_1 a, ... X_P_n :: P_n a,
        X_op_1 :: t_1, ..., X_op_m :: t_m
    }
    

    Aquí X_P_iy X_op_ison selectores . Es decir, un valor de tipo que se X aaplica X_P_1al valor devolverá el valor almacenado en ese campo, por lo que son funciones con tipo X a -> P_i a(o X a -> t_i).

    Para una anología muy aproximada , podría pensar en los valores para type X acomo structsy luego, si xes de type, X alas expresiones:

    X_P_1 x
    X_op_1 x
    

    podría verse como:

    x.X_P_1
    x.X_op_1
    

    (Es fácil usar solo campos posicionales en lugar de campos con nombre, pero los campos con nombre son más fáciles de manejar en los ejemplos y evitan algún código de placa de caldera).

  • Dada una declaración de instancia:

    instance (C_1 a_1, ..., C_n a_n) => X (T a_1 ... a_n) where
        op_1 = ...; ...;  op_m = ...
    

    Puede traducirlo a una función que, dado los diccionarios para las C_1 a_1, ..., C_n a_nclases, devuelve un valor de diccionario (es decir, un valor de tipo X a) para el tipo T a_1 ... a_n.

    En otras palabras, la instancia anterior se puede traducir a una función como:

    f :: C_1 a_1 -> ... -> C_n a_n -> X (T a_1 ... a_n)
    

    (Tenga en cuenta que npuede ser 0).

    Y de hecho podemos definirlo como:

    f c1 ... cN = X {X_P_1=get_P_1_T, X_P_n=get_P_n_T,
                     X_op_1=op_1, ..., X_op_m=op_m}
        where
            op_1 = ...
            ...
            op_m = ...
    

    donde op_1 = ...a op_m = ...son las definiciones que se encuentran en la instancedeclaración y el get_P_i_Tson las funciones definidas por la P_iinstancia del Ttipo (deben existen porque P_is son superclases de X).

  • Dada una llamada a una función sobrecargada:

    add :: Num a => a -> a -> a
    add x y = x + y
    

    Podemos pasar explícitamente los diccionarios relativos a las restricciones de clase y obtener una llamada equivalente:

    add :: Num a -> a -> a -> a
    add dictNum x y = ((+) dictNum) x y
    

    Observe cómo las restricciones de clase simplemente se convirtieron en un nuevo argumento. El +en el programa traducido es el selector como se explicó anteriormente. En otras palabras, la addfunción traducida , dado el diccionario para el tipo de argumento, primero "desempaquetará" la función real para calcular el resultado usando (+) dictNumy luego aplicará esta función a los argumentos.

Este es solo un esbozo muy rápido sobre todo el asunto. Si está interesado, debe leer los artículos de Simon Peyton Jones et al.

Creo que un enfoque similar podría usarse para sobrecargar en otros idiomas también.

Sin embargo, esto muestra que, si su definición de azúcar sintáctico es (1), la sobrecarga es azúcar sintáctico . Porque puedes deshacerte de él.

Sin embargo, el programa traducido pierde información sobre el programa original. Por ejemplo, no exige que existan instancias para las clases principales. (Aunque las operaciones para extraer los diccionarios de los padres todavía deben ser de ese tipo, puede pasar undefinedu otros valores polimórficos para que pueda generar un valor X ysin generar los valores para P_i y, por lo que la traducción no pierde todo El tipo de seguridad). Por lo tanto, no es azúcar sintáctico de acuerdo con (2)

En cuanto a (3). No sé si la respuesta debería ser un sí o un no.

Yo diría que no porque, por ejemplo, una declaración de instancia se convierte en una definición de función. Las funciones que están sobrecargadas obtienen un nuevo parámetro (lo que significa que cambia tanto la definición como todas las llamadas).

Yo diría que sí porque los dos programas todavía se asignan uno a uno, por lo que la "estructura" en realidad no cambia tanto.


Dicho esto, diría que las ventajas pragmáticas introducidas por la sobrecarga son tan grandes que usar un término "despectivo" como "azúcar sintáctico" no parece correcto.

Puede traducir toda la sintaxis de Haskell a un lenguaje Core muy simple (que en realidad se hace al compilar), por lo que la mayoría de la sintaxis de Haskell podría verse como "azúcar sintáctico" para algo que es solo cálculo lambda más un poco de nuevas construcciones. Sin embargo, podemos estar de acuerdo en que los programas de Haskell son mucho más fáciles de manejar y muy concisos, mientras que los programas traducidos son bastante más difíciles de leer o pensar.

Bakuriu
fuente
2

Si el despacho se resuelve en tiempo de compilación, dependiendo solo del tipo estático de la expresión del argumento, entonces ciertamente puede argumentar que es "azúcar sintáctico" reemplazando dos métodos diferentes con nombres diferentes, siempre que el programador "conozca" el tipo estático y podría usar el nombre de método correcto en lugar del nombre sobrecargado. Es también una forma de polimorfismo estático, pero en forma limitada que normalmente no es muy potente.

Por supuesto, sería una molestia tener que cambiar los nombres de los métodos que llama cada vez que cambia el tipo de una variable, pero, por ejemplo, en el lenguaje C se considera una molestia manejable, por lo que C no tiene una sobrecarga de funciones (aunque ahora tiene macros genéricas).

En las plantillas de C ++, y en cualquier lenguaje que haga una deducción de tipo estático no trivial, realmente no puede argumentar que esto es "azúcar sintáctico" a menos que también argumente que la deducción de tipo estático es "azúcar sintáctico". Sería una molestia no tener plantillas, y en el contexto de C ++ sería una "molestia inmanejable", ya que son tan idiotas para el lenguaje y sus bibliotecas estándar. Entonces, en C ++ es más que un buen ayudante, es importante para el estilo del lenguaje, y creo que hay que llamarlo más que "azúcar sintáctico".

En Java, podría considerarlo más que una simple conveniencia considerando, por ejemplo, cuántas sobrecargas hay de PrintStream.printy PrintStream.println. Pero entonces, hay tantosDataInputStream.readX métodos ya que Java no se sobrecarga en el tipo de retorno, por lo que, en cierto sentido, es solo por conveniencia. Esos son todos para tipos primitivos.

No recuerdo lo que ocurre en Java si tengo clases Ay Bque se extiende O, sobrecargo métodos foo(O), foo(A)y foo(B), a continuación, en un genérico con <T extends O>que llamo foo(t), donde tes una instancia de T. En el caso en que Tes ACómo llego envío basado en la sobrecarga o se trata como si llamara foo(O)?

Si es lo primero, entonces las sobrecargas del método Java son mejores que el azúcar de la misma manera que las sobrecargas de C ++. Usando su definición, supongo que en Java podría escribir localmente una serie de verificaciones de tipo (lo que sería frágil, porque las nuevas sobrecargas foorequerirían verificaciones adicionales). Además de aceptar esa fragilidad, no puedo hacer un cambio local en el sitio de la llamada para hacerlo bien; en cambio, tendría que renunciar a escribir código genérico. Yo diría que prevenir el código hinchado podría ser un azúcar sintáctico, pero prevenir el código frágil es más que eso. Por esa razón, el polimorfismo estático en general es más que solo azúcar sintáctico. La situación en un idioma en particular puede ser diferente, dependiendo de qué tan lejos el idioma le permita llegar al "no conocer" el tipo estático.

Steve Jessop
fuente
En Java, las sobrecargas se resuelven en tiempo de compilación. Dado el uso de borrado tipo, sería imposible para ellos ser de otra manera. Además, incluso sin el tipo de borrado, si T:Animales es escribir SiameseCaty sobrecargas existentes Cat Foo(Animal), SiameseCat Foo(Cat)y Animal Foo(SiameseCat), lo que sobrecarga deben seleccionarse si Tes SiameseCat?
supercat
@supercat: tiene sentido. Así que podría haber descubierto la respuesta sin recordar (o, por supuesto, ejecutarla). Por lo tanto, las sobrecargas de Java no son mejores que el azúcar de la misma manera que las sobrecargas de C ++ se relacionan con el código genérico. Sigue siendo posible que haya otra forma en que sean mejores que solo una transformación local. Me pregunto si debería cambiar mi ejemplo a C ++, o dejarlo como un Java imaginado que no es Java real.
Steve Jessop
Las sobrecargas pueden ser útiles en los casos en que los métodos tienen argumentos opcionales, pero también pueden ser peligrosos. Supongamos que la línea long foo=Math.round(bar*1.0001)*5se cambia a long foo=Math.round(bar)*5. ¿Cómo afectaría eso a la semántica si bares igual, por ejemplo, 123456789L?
supercat
@supercat Yo diría que el verdadero peligro es la conversión implícita de longa double.
Doval
@Doval: double¿ A ?
supercat
1

Parece que el "azúcar sintáctico" suena despectivo, como inútil o frívolo. Es por eso que la pregunta desencadena muchas respuestas negativas.

Pero tiene razón, la sobrecarga de métodos no agrega ninguna característica al idioma, excepto la posibilidad de usar el mismo nombre para diferentes métodos. Puede hacer explícito el tipo de parámetro, el programa seguirá funcionando igual.

Lo mismo se aplica a los nombres de paquetes. La cadena es solo azúcar sintáctica para java.lang.String.

De hecho, un método como

void fun(int i, String c);

en la clase MyClass debería llamarse algo así como "my_package_MyClass_fun_int_java_lang_String". Esto identificaría el método de manera única. (La JVM hace algo así internamente). Pero no quieres escribir eso. Es por eso que el compilador le permitirá escribir diversión (1, "uno") e identificar qué método es.

Sin embargo, hay una cosa que puede hacer con la sobrecarga: si sobrecarga un método con el mismo número de argumentos, el compilador descubrirá automáticamente qué versión se adapta mejor al argumento dado por argumentos coincidentes, no solo con tipos iguales, sino también donde El argumento dado es una subclase del argumento declarado.

Si tienes dos procedimientos sobrecargados

addParameter(String name, Object value);
addParameter(String name, Date value);

no necesita saber que existe una versión específica del procedimiento para las fechas. addParameter ("hello", "world) llamará a la primera versión, addParameter (" now ", new Date ()) llamará a la segunda.

Por supuesto, debe evitar sobrecargar un método con otro método que haga algo completamente diferente.

Florian F
fuente
1

Curiosamente, la respuesta a esta pregunta dependerá del idioma.

Específicamente, existe una interacción entre la sobrecarga y la programación genérica. (*), y dependiendo de cómo se implemente la programación genérica, podría ser solo azúcar sintáctica (Rust) o absolutamente necesaria (C ++).

Es decir, cuando la programación genérica se implementa con interfaces explícitas (en Rust o Haskell, esas serían clases de tipo), la sobrecarga es simplemente azúcar sintáctica; o incluso podría no ser parte del lenguaje.

Por otro lado, cuando la programación genérica se implementa con tipificación de pato (ya sea dinámico o estático), el nombre del método es un contrato esencial y, por lo tanto, la sobrecarga es obligatoria para que el sistema funcione.

(*) Utilizado en el sentido de escribir un método una vez, para operar sobre varios tipos de manera uniforme.

Matthieu M.
fuente
0

En algunos idiomas, sin duda, es simplemente azúcar sintáctico. Sin embargo, lo que es un azúcar depende de su punto de vista. Dejaré esta discusión para más adelante en esta respuesta.

Por ahora me gustaría señalar que en algunos idiomas ciertamente no es azúcar sintáctica. Al menos no sin requerir que uses una lógica / algoritmo completamente diferente para implementar lo mismo. Es como afirmar que la recursividad es azúcar sintáctica (que es porque puedes escribir todo el algoritmo recursivo con un bucle y una pila).

Un ejemplo de uso muy difícil de reemplazar proviene de un lenguaje que, irónicamente, no llama a esta característica "sobrecarga de funciones". En cambio, se llama "coincidencia de patrones" (que puede verse como un superconjunto de sobrecarga porque podemos sobrecargar no solo los tipos sino los valores).

Aquí está la implementación ingenua clásica de la función Fibonacci en Haskell:

fib 0 = 0
fib 1 = 1
fib n = fib (n-1) + fib (n-2)

Podría decirse que las tres funciones se pueden reemplazar con un if / else como se hace comúnmente en cualquier otro idioma. Pero eso hace que la definición sea completamente simple:

fib n = fib (n-1) + fib (n-2)

mucho más desordenado y no expresa directamente la noción matemática de la secuencia de Fibonacci.

Entonces, a veces puede ser azúcar de sintaxis si el único uso es permitirle llamar a una función con diferentes argumentos. Pero a veces es mucho más fundamental que eso.


Ahora para la discusión de para qué operador la sobrecarga puede ser un azúcar. Ha identificado un caso de uso: puede usarse para implementar funciones similares que toman diferentes argumentos. Entonces:

function print (string x) { stdout.print(x) };
function print (number x) { stdout.print(x.toString) };

alternativamente se puede implementar como:

function printString (string x) {...}
function printNumber (number x) {...}

o incluso:

function print (auto x) {
    if (x instanceof String) {...}
    if (x instanceof Number) {...}
}

Pero la sobrecarga del operador también puede ser un azúcar para implementar argumentos opcionales (algunos idiomas tienen sobrecarga del operador pero no argumentos opcionales):

function print (string x) {...}
function print (string x, stream io) {...}

puede usarse para implementar:

function print (string x, stream io=stdout) {...}

En dicho lenguaje ("lenguaje Ferite" de Google), eliminar la sobrecarga del operador elimina drásticamente una característica: los argumentos opcionales. Concedido en lenguajes con ambas características (c ++) eliminar uno u otro no tendrá ningún efecto neto ya que cualquiera de ellos puede usarse para implementar argumentos opcionales.

slebetman
fuente
Haskell es un buen ejemplo de por qué la sobrecarga del operador no es azúcar sintáctica, pero creo que un mejor ejemplo sería la deconstrucción de un tipo de datos algebraicos con coincidencia de patrones (algo que hasta donde yo sé es imposible sin una coincidencia de patrones).
11684
@ 11684: ¿Puede señalar un ejemplo? Sinceramente, no conozco a Haskell en absoluto, pero encontré que su combinación de patrones es sublimemente elegante cuando vi ese ejemplo de fib (en computerphile en youtube).
slebetman
Dado un tipo de datos como el data PaymentInfo = CashOnDelivery | Adress String | UserInvoice CustomerInfoque puede hacer coincidir patrones en los constructores de tipos.
11684
De esta manera: getPayment :: PaymentInfo -> a getPayment CashOnDelivery = error "Should have been paid already" getPayment (Adress addr) = -- code to notify administration to send a bill getPayment (UserInvoice cust) = --whatever. I took the data type from a Haskell tutorial and have no idea what an invoice is. Espero que este comentario sea algo comprensible.
11684
0

Creo que es el azúcar sintáctico simple en la mayoría de los idiomas (al menos todo lo que sé ...) ya que todos requieren una llamada de función sin ambigüedad en el momento de la compilación. Y el compilador simplemente reemplaza la llamada a la función con un puntero explícito a la firma de implementación correcta.

Ejemplo en Java:

String s; int i;
mangle(s);  // Translates to CALL ('mangle':LString):(s)
mangle(i);  // Translates to CALL ('mangle':Lint):(i)

Entonces, al final, podría reemplazarse completamente por una simple macro compiladora con buscar y reemplazar, reemplazando Function mangle sobrecargado con mangle_String y mangle_int, ya que la lista de argumentos es parte del eventual identificador de función, esto es prácticamente lo que sucede -> y por lo tanto, es solo azúcar sintáctico.

Ahora, si hay un lenguaje, donde la función está realmente determinada en tiempo de ejecución, como con los Métodos anulados en los objetos, esto sería diferente. Pero no creo que haya tal lenguaje, ya que method.overloading es propenso a la ambigüedad, que el compilador no puede resolver y que el programador debe manejar con un reparto explícito. Esto no se puede hacer en tiempo de ejecución.

Falco
fuente
0

En Java, la información de tipo se compila y cuál de las sobrecargas se llama se decide en el momento de la compilación.

El siguiente es un fragmento de sun.misc.Unsafe(la utilidad para Atomics) como se ve en el editor de archivos de clase de Eclipse.

  // Method descriptor #143 (Ljava/lang/Object;I)I (deprecated)
  // Stack: 4, Locals: 3
  @java.lang.Deprecated
  public int getInt(java.lang.Object arg0, int arg1);
    0  aload_0 [this]
    1  aload_1 [arg0]
    2  iload_2 [arg1]
    3  i2l
    4  invokevirtual sun.misc.Unsafe.getInt(java.lang.Object, long) : int [231]
    7  ireturn
      Line numbers:
        [pc: 0, line: 213]

como puede ver, la información de tipo del método que se llama (línea 4) se incluye en la llamada.

Esto significa que podría crear un compilador de Java que tome información de tipo. Por ejemplo, utilizando una notación de este tipo, la fuente anterior sería:

@Deprecated
public in getInt(Object arg0, int arg1){
     return getInt$(Object,long)(arg0, arg1);
}

y el elenco a largo sería opcional.

En otros lenguajes compilados de tipo estático, verá una configuración similar en la que el compilador decidirá qué sobrecarga se llamará según los tipos y la incluirá en el enlace / llamada.

La excepción son las bibliotecas dinámicas de C donde la información de tipo no está incluida y el intentar crear una función sobrecargada hará que el vinculador se queje.

monstruo de trinquete
fuente