¿Por qué no se permite la sobrecarga con tipos de retorno? (al menos en los idiomas utilizados habitualmente)

9

No conozco todos los lenguajes de programación, pero está claro que generalmente no se admite la posibilidad de sobrecargar un método teniendo en cuenta su tipo de retorno (suponiendo que sus argumentos sean del mismo número y tipo).

Me refiero a algo como esto:

 int method1 (int num)
 {

 }
 long method1 (int num)
 {

 }

No es que sea un gran problema para la programación, pero en algunas ocasiones me hubiera gustado.

Obviamente, no habría forma de que esos lenguajes admitieran eso sin una forma de diferenciar qué método se llama, pero la sintaxis para eso puede ser tan simple como algo como [int] method1 (num) o [long] method1 (num) de esa manera el compilador sabría cuál sería el que se llamaría.

No sé cómo funcionan los compiladores, pero eso no parece ser tan difícil de hacer, así que me pregunto por qué algo así no suele implementarse.

¿Cuáles son las razones por las que algo así no es compatible?

usuario2638180
fuente
Tal vez su pregunta sería mejor con un ejemplo donde no existen conversiones implícitas entre los dos tipos de retorno, por ejemplo, clases Fooy Bar.
Bueno, ¿por qué sería útil esta característica?
James Youngman
3
@JamesYoungman: Por ejemplo, para analizar cadenas en diferentes tipos, puede tener un método int read (String s), float read (String s), etc. Cada variación sobrecargada del método realiza un análisis para el tipo apropiado.
Giorgio
Por cierto, esto solo es un problema con los idiomas tipados estáticamente. Tener múltiples tipos de retorno es bastante rutinario en lenguajes de tipo dinámico como Javascript o Python.
Gort the Robot
1
@ StevenBurnap, um, no. Con, por ejemplo, JavaScript, no puede hacer la sobrecarga de funciones en absoluto. Por lo tanto, esto solo es un problema con los idiomas que admiten la sobrecarga de nombres de funciones.
David Arno

Respuestas:

19

Complica la verificación de tipo.

Cuando solo permite la sobrecarga basada en tipos de argumento, y solo permite deducir tipos variables de sus inicializadores, toda su información de tipo fluye en una dirección: hacia arriba en el árbol de sintaxis.

var x = f();

given      f   : () -> int  [upward]
given      ()  : ()         [upward]
therefore  f() : int        [upward]
therefore  x   : int        [upward]

Cuando permite que la información de tipo viaje en ambas direcciones, como deducir el tipo de una variable de su uso, necesita un solucionador de restricciones (como el Algoritmo W, para sistemas de tipo Hindley-Milner) para determinar el tipo.

var x = parse("123");
print_int(x);

given      parse        : string -> T  [upward]
given      "123"        : string       [upward]
therefore  parse("123") : ∃T           [upward]
therefore  x            : ∃T           [upward]
given      print_int    : int -> ()    [upward]
therefore  print_int(x) : ()           [upward]
therefore  int -> ()    = ∃T -> ()     [downward]
therefore  ∃T           = int          [downward]
therefore  x            : int          [downward]

Aquí, teníamos que dejar el tipo de xcomo una variable de tipo sin resolver ∃T, donde todo lo que sabemos al respecto es que es analizable. Solo más tarde, cuando xse usa en un tipo concreto, tenemos suficiente información para resolver la restricción y determinar eso ∃T = int, lo que propaga la información del tipo en el árbol de sintaxis desde la expresión de llamada hacia x.

Si no pudiéramos determinar el tipo de x, este código también se sobrecargaría (por lo que la persona que llama determinaría el tipo) o tendríamos que informar un error sobre la ambigüedad.

A partir de esto, un diseñador de idiomas puede concluir:

  • Agrega complejidad a la implementación.

  • Hace que la verificación de tipos sea más lenta, en casos patológicos, exponencialmente.

  • Es más difícil producir buenos mensajes de error.

  • Es muy diferente del status quo.

  • No tengo ganas de implementarlo.

Jon Purdy
fuente
1
Además: será tan difícil de entender que, en algunos casos, su compilador tomará decisiones que el programador no esperaba, lo que provocará errores difíciles de encontrar.
gnasher729
1
@ gnasher729: No estoy de acuerdo. Regularmente uso Haskell, que tiene esta característica, y nunca he sido mordido por su elección de sobrecarga (por ejemplo, instancia de tipo de clase). Si algo es ambiguo, solo me obliga a agregar una anotación de tipo. Y todavía elegí implementar inferencia de tipo completo en mi idioma porque es increíblemente útil. Esta respuesta fue yo jugando al abogado del diablo.
Jon Purdy
4

Porque es ambiguo. Usando C # como ejemplo:

var foo = method(42);

¿Qué sobrecarga debemos usar?

Ok, tal vez eso fue un poco injusto. El compilador no puede determinar qué tipo usar si no lo decimos en su lenguaje hipotético. Por lo tanto, la escritura implícita es imposible en su idioma y hay métodos anónimos y Linq junto con él ...

¿Que tal este? (Firmas ligeramente redefinidas para ilustrar el punto).

short method(int num) { ... }
int method(int num) { ... }

....

long foo = method(42);

¿Deberíamos usar la intsobrecarga o la shortsobrecarga? Simplemente no lo sabemos, tendremos que especificarlo con su [int] method1(num)sintaxis. Lo cual es un poco difícil de analizar y escribir para ser honesto.

long foo = [int] method(42);

La cuestión es que es una sintaxis sorprendentemente similar a un método genérico en C #.

long foo = method<int>(42);

(C ++ y Java tienen características similares).

En resumen, los diseñadores de idiomas eligieron resolver el problema de una manera diferente para simplificar el análisis y habilitar funciones de lenguaje mucho más potentes.

Dices que no sabes mucho sobre compiladores. Recomiendo aprender sobre gramáticas y analizadores. Una vez que comprenda qué es una gramática libre de contexto, tendrá una idea mucho mejor de por qué la ambigüedad es algo malo.

RubberDuck
fuente
Buen punto sobre los genéricos, aunque pareces confundir shorty int.
Robert Harvey
Sí @RobertHarvey tienes razón. Estaba luchando por un ejemplo para ilustrar el punto. Funcionaría mejor si se methoddevuelve un short o int, y el tipo se definió como long.
RubberDuck
Eso parece un poco mejor.
RubberDuck
No compro sus argumentos de que no puede tener inferencia de tipos, subrutinas anónimas o comprensiones de mónada en un idioma con sobrecarga de tipo de retorno. Haskell lo hace, y Haskell tiene los tres. También tiene polimorfismo paramétrico. Su punto sobre long/ int/ shorttiene más que ver con las complejidades del subtipo y / o las conversiones implícitas que con la sobrecarga del tipo de retorno. Después de todo, los literales numéricos están sobrecargados en su tipo de retorno en C ++, Java, C♯ y muchos otros, y eso no parece presentar un problema. Simplemente puede inventar una regla: por ejemplo, elija el tipo más específico / general.
Jörg W Mittag
@ JörgWMittag mi punto no era que lo haría imposible, solo que hace que las cosas sean innecesariamente complejas.
RubberDuck
0

Todas las características del lenguaje agregan complejidad, por lo que tienen que proporcionar un beneficio suficiente para justificar las trampas inevitables, los casos de esquina y la confusión del usuario que crea cada característica. Para la mayoría de los idiomas, este simplemente no proporciona suficientes beneficios para justificarlo.

En la mayoría de los idiomas, es de esperar que la expresión method1(2)tenga un tipo definido y un valor de retorno más o menos predecible. Pero si permite la sobrecarga en los valores de retorno, eso significa que es imposible saber qué significa esa expresión en general sin tener en cuenta el contexto que la rodea. Considere lo que sucede cuando tiene un unsigned long long foo()método cuya implementación termina con return method1(2)? ¿Debería eso llamar la longsobrecarga de retorno o la intsobrecarga de retorno o simplemente dar un error del compilador?

Además, si tiene que ayudar al compilador anotando el tipo de retorno, no solo está inventando más sintaxis (lo que aumenta todos los costos antes mencionados para permitir que la característica exista), sino que efectivamente está haciendo lo mismo que crear dos métodos con nombres diferentes en un lenguaje "normal". ¿Es [long] method1(2)más intuitivo que long_method1(2)?


Por otro lado, algunos lenguajes funcionales como Haskell con sistemas de tipos estáticos muy fuertes permiten este tipo de comportamiento, porque su inferencia de tipos es lo suficientemente potente como para que rara vez necesite anotar el tipo de retorno en esos idiomas. Pero eso solo es posible porque esos idiomas realmente imponen la seguridad de los tipos, más que cualquier lenguaje tradicional, además de requerir que todas las funciones sean puras y referencialmente transparentes. No es algo que sea factible en la mayoría de los idiomas de OOP.

Ixrec
fuente
2
"junto con la exigencia de que todas las funciones sean puras y referencialmente transparentes": ¿Cómo facilita esto la sobrecarga del tipo de retorno?
Giorgio
@Giorgio No lo hace: Rust no impone la función de la pureza de la función y todavía puede devolver la sobrecarga de tipos (aunque su sobrecarga en Rust es muy diferente a la de otros idiomas (solo puede sobrecargar usando plantillas))
Idan Arye
La parte [larga] y [int] debía tener una forma de llamar explícitamente al método, en la mayoría de los casos, cómo debería llamarse podría inferirse directamente del tipo de variable a la que se asigna la ejecución del método.
user2638180
0

Que está disponible en Swift y funciona bien allí. Obviamente, no puede tener un tipo ambiguo en ambos lados, por lo que debe ser conocido a la izquierda.

Usé esto en una simple API de codificación / decodificación .

public protocol HierDecoder {
  func dech() throws -> String
  func dech() throws -> Int
  func dech() throws -> Bool

Eso significa que las llamadas donde se conocen tipos de parámetros, como la initde un objeto, funcionan de manera muy simple:

    private static let typeCode = "ds"
    static func registerFactory() {
        HierCodableFactories.Register(key:typeCode) {
            (from) -> HierCodable in
            return try tgDrawStyle(strokeColor:from.dech(), fillColor:from.dechOpt(), lineWidth:from.dech(), glowWidth: from.dech())
        }
    }
    func typeKey() -> String { return tgDrawStyle.typeCode }
    func encode(to:HierEncoder) {
        to.ench(strokeColor)
        to.enchOpt(fillColor)
        to.ench(lineWidth)
        to.ench(glowWidth)
    }

Si presta mucha atención, notará la dechOptllamada anterior. Descubrí por las malas que sobrecargar el mismo nombre de función donde el diferenciador estaba devolviendo un opcional era demasiado propenso a errores ya que el contexto de la llamada podría introducir una expectativa de que era opcional.

Andy Dent
fuente
-5
int main() {
    auto var1 = method1(1);
}
DeadMG
fuente
En ese caso, el compilador podría a) rechazar la llamada porque es ambigua b) elegir el primero / último c) dejar el tipo de var1y proceder con la inferencia de tipo y tan pronto como alguna otra expresión determine el tipo de var1uso que se utiliza para seleccionar el implementación correcta En la línea inferior, mostrar un caso en el que la inferencia de tipos no es trivial rara vez prueba un punto distinto de la inferencia de tipos que generalmente no es trivial.
back2dos
1
No es un argumento fuerte. Rust, por ejemplo, hace un uso intensivo de la inferencia de tipos y, en algunos casos, especialmente con los genéricos, no puede decir qué tipo necesita. En tales casos, simplemente tiene que dar explícitamente el tipo en lugar de confiar en la inferencia de tipos.
8bittree
1
Uh ... eso no demuestra un tipo de retorno sobrecargado. method1debe declararse para devolver un tipo particular.
Gort the Robot