¿Es una mala idea devolver diferentes tipos de datos desde una sola función en un lenguaje de tipo dinámico?

64

Mi idioma principal está escrito estáticamente (Java). En Java, debe devolver un solo tipo de cada método. Por ejemplo, no puede tener un método que devuelva condicionalmente un String o devuelva condicionalmente un Integer . Pero en JavaScript, por ejemplo, esto es muy posible.

En un lenguaje estáticamente tipado entiendo por qué esto es una mala idea. Si cada método devolvió Object (el padre común del que se heredan todas las clases), entonces usted y el compilador no tienen idea de con qué está tratando. Tendrás que descubrir todos tus errores en el tiempo de ejecución.

Pero en un lenguaje de tipo dinámico, puede que ni siquiera haya un compilador. En un lenguaje de tipo dinámico, no es obvio para mí por qué una función que devuelve múltiples tipos es una mala idea. Mi experiencia en lenguajes estáticos me hace evitar escribir tales funciones, pero me temo que tengo una mentalidad cercana sobre una función que podría hacer que mi código sea más limpio de una manera que no puedo ver.

Editar : eliminaré mi ejemplo (hasta que pueda pensar en uno mejor). Creo que está guiando a la gente a responder a un punto que no estoy tratando de hacer.

    
pregunta Daniel Kaplan 27.01.2014 - 18:45

14 respuestas

43

A diferencia de otras respuestas, hay casos en los que es aceptable devolver diferentes tipos.

Ejemplo 1

sum(2, 3) → int
sum(2.1, 3.7) → float

En algunos idiomas tipificados estáticamente, esto implica sobrecargas, por lo que podemos considerar que existen varios métodos, cada uno de los cuales devuelve el tipo fijo predefinido. En lenguajes dinámicos, esta puede ser la misma función, implementada como:

var sum = function (a, b) {
    return a + b;
};

La misma función, diferentes tipos de valor de retorno.

Ejemplo 2

Imagine que obtiene una respuesta de un componente OpenID / OAuth. Algunos proveedores de OpenID / OAuth pueden contener más información, como la edad de la persona.

var user = authProvider.findCurrent();
// user is now:
// {
//     provider: 'Facebook',
//     name: {
//         firstName: 'Hello',
//         secondName: 'World',
//     },
//     email: '[email protected]',
//     age: 27
// }

Otros tendrían el mínimo, sería una dirección de correo electrónico o el seudónimo.

var user = authProvider.findCurrent();
// user is now:
// {
//     provider: 'Google',
//     email: '[email protected]'
// }

De nuevo, la misma función, resultados diferentes.

Aquí, el beneficio de devolver diferentes tipos es especialmente importante en un contexto en el que no te interesan los tipos y las interfaces, sino lo que contienen los objetos. Por ejemplo, imaginemos que un sitio web contiene lenguaje maduro. Entonces el findCurrent() se puede usar de esta manera:

var user = authProvider.findCurrent();
if (user.age || 0 >= 16) {
    // The person can stand mature language.
    allowShowingContent();
} else if (user.age) {
    // OpenID/OAuth gave the age, but the person appears too young to see the content.
    showParentalAdvisoryRequestedMessage();
} else {
    // OpenID/OAuth won't tell the age of the person. Ask the user himself.
    askForAge();
}

Refactorizar esto en un código donde cada proveedor tendrá su propia función que devolverá un tipo fijo bien definido, no solo degradará la base del código y causará la duplicación del código, sino que también no aportará ningún beneficio. Uno puede terminar haciendo horrores como:

var age;
if (['Facebook', 'Yahoo', 'Blogger', 'LiveJournal'].contains(user.provider)) {
    age = user.age;
}
    
respondido por el Arseni Mourzenko 27.01.2014 - 19:58
31

En general, es una mala idea por las mismas razones que el equivalente moral en un lenguaje estático es una mala idea: no tiene idea de qué tipo concreto se devuelve, por lo que no tiene idea de lo que puede hacer con el resultado ( Aparte de las pocas cosas que se pueden hacer con cualquier valor). En un sistema de tipo estático, tiene anotaciones verificadas por el compilador de los tipos de retorno y similares, pero en un lenguaje dinámico todavía existe el mismo conocimiento: es simplemente informal y se almacena en el cerebro y la documentación en lugar de en el código fuente.

Sin embargo, en muchos casos, existe una rima y una razón por la cual se devuelve el tipo y el efecto es similar a la sobrecarga o al polimorfismo paramétrico en los sistemas de tipo estático. En otras palabras, el tipo de resultado es predecible, simplemente no es tan fácil de expresar.

Pero tenga en cuenta que puede haber otras razones por las que una función específica está mal diseñada: por ejemplo, una función sum que devuelve falso en entradas no válidas es una mala idea principalmente porque ese valor de retorno es inútil y propenso a errores (0 < - > falsa confusión).

    
respondido por el user7043 27.01.2014 - 19:01
26

En los idiomas dinámicos, no debe preguntar si devuelve tipos diferentes pero objetos con diferentes API . Como a la mayoría de los lenguajes dinámicos no les importan los tipos, pero use varias versiones de tipografía de pato .

Al devolver tipos diferentes tiene sentido

Por ejemplo, este método tiene sentido:

def load_file(file): 
    if something: 
       return ['a ', 'list', 'of', 'strings'] 
    return open(file, 'r')

Porque tanto el archivo como una lista de cadenas son (en Python) iterables que devuelven una cadena. Tipos muy diferentes, la misma API (a menos que alguien intente llamar a los métodos de archivo en una lista, pero esta es una historia diferente).

Puedes devolver condicionalmente list o tuple ( tuple es una lista inmutable en python).

Formalmente incluso haciendo:

def do_something():
    if ...: 
        return None
    return something_else

o:

function do_something(){
   if (...) return null; 
   return sth;
}

devuelve diferentes tipos, ya que python None y Javascript null son tipos por derecho propio.

Todos estos casos de uso tendrían su contraparte en lenguajes estáticos, la función solo devolvería una interfaz adecuada.

Cuando devolver objetos con diferentes API condicionalmente es una buena idea

En cuanto a si devolver API diferentes es una buena idea, IMO en la mayoría de los casos no tiene sentido. El único ejemplo sensato que se me ocurre es algo cercano a lo que dijo @MainMa : cuando su API puede proporcionar una cantidad variable detalle podría tener sentido devolver más detalles cuando esté disponible.

    
respondido por el jb. 28.01.2014 - 00:28
9

Tu pregunta me da ganas de llorar un poco. No por el ejemplo de uso que proporcionó, sino porque alguien, sin saberlo, tomará este enfoque demasiado lejos. Está a un paso del código ridículamente imposible de mantener.

El tipo de caso de uso de la condición de error tiene sentido, y el patrón nulo (todo tiene que ser un patrón) en idiomas tipificados estáticamente hace el mismo tipo de cosas. Su llamada de función devuelve un object o devuelve null .

Pero es un paso corto para decir "Voy a usar esto para crear un patrón de fábrica " y devolver foo o bar o baz dependiendo del estado de ánimo de la función . Depurar esto se convertirá en una pesadilla cuando la persona que llama espera foo pero recibe bar .

Así que no creo que te estén cerrando la mente. Estás siendo muy cauteloso al usar las funciones del idioma.

Disclosure: mi experiencia es en idiomas estáticos, y generalmente he trabajado en equipos más grandes y diversos donde la necesidad de un código de mantenimiento era bastante alta. Así que mi perspectiva probablemente también esté sesgada.

    
respondido por el GlenH7 27.01.2014 - 19:03
6

El uso de Genéricos en Java le permite devolver un tipo diferente, al tiempo que conserva la seguridad de tipo estático. Simplemente especifique el tipo que desea devolver en el parámetro de tipo genérico de la llamada de función.

Si puede usar un enfoque similar en Javascript es, por supuesto, una pregunta abierta. Dado que Javascript es un lenguaje de tipo dinámico, la opción obvia es devolver un object .

Si desea saber dónde podría funcionar un escenario de retorno dinámico cuando está acostumbrado a trabajar en un lenguaje de tipo estático, considere buscar la palabra clave dynamic en C #. Rob Conery pudo escribir un Asignador Relacional de Objetos en 400 líneas de código usando la palabra clave dynamic .

Por supuesto, todo lo que dynamic realmente hace es envolver una variable object con algún tipo de seguridad de tiempo de ejecución.

    
respondido por el Robert Harvey 27.01.2014 - 18:53
4

Creo que es una mala idea devolver diferentes tipos condicionalmente. Una de las formas en que esto aparece frecuentemente para mí es si la función puede devolver uno o más valores. Si solo es necesario devolver un valor, puede parecer razonable simplemente devolver el valor, en lugar de empaquetarlo en una matriz, para evitar tener que descomprimirlo en la función de llamada. Sin embargo, esto (y la mayoría de las otras instancias de esto) impone a la persona que llama distinguir entre y manejar ambos tipos. La función será más fácil de razonar si siempre devuelve el mismo tipo.

    
respondido por el user116240 27.01.2014 - 19:13
4

La "mala práctica" existe independientemente de si su idioma está escrito de forma estática. El lenguaje estático hace más para alejarlo de estas prácticas, y puede encontrar más usuarios que se quejan de "malas prácticas" en un lenguaje estático porque es un lenguaje más formal. Sin embargo, los problemas subyacentes están allí en un lenguaje dinámico, y puede determinar si están justificados.

Aquí está la parte objetable de lo que propones. Si no sé qué tipo se devuelve, no puedo usar el valor de retorno inmediatamente. Tengo que "descubrir" algo al respecto.

total = sum_of_array([20, 30, 'q', 50])
if (type_of(total) == Boolean) {
  display_error(...)
} else {
  record_number(total)
}

A menudo, este tipo de cambio en el código es simplemente una mala práctica. Hace que el código sea más difícil de leer. En este ejemplo, verá por qué es popular lanzar y capturar casos de excepción. Dicho de otra manera: si su función no puede hacer lo que dice que hace, no debería regresar con éxito . Si llamo a tu función, quiero hacer esto:

total = sum_of_array([20, 30, 'q', 50])
display_number(total)

Debido a que la primera línea devuelve con éxito, asumo que total realmente contiene la suma de la matriz. Si no vuelve con éxito, saltamos a alguna otra página de mi programa.

Usemos otro ejemplo que no se trata solo de propagar errores. Tal vez sum_of_array intenta ser inteligente y devolver una cadena legible por humanos en algunos casos, como "¡Esa es mi combinación de casillero!" si y solo si la matriz es [11,7,19]. Estoy teniendo problemas para pensar en un buen ejemplo. De todos modos, se aplica el mismo problema. Debe inspeccionar el valor de retorno antes de poder hacer algo con él:

total = sum_of_array([20, 30, 40, 50])
if (type_of(total) == String) {
  write_message(total)
} else {
  record_number(total)
}

Podría argumentar que sería útil para la función devolver un entero o un flotante, por ejemplo:

sum_of_array(20, 30, 40) -> int
sum_of_array(23.45, 45.67, 67.789044) -> float

Pero esos resultados no son tipos diferentes, en lo que a usted respecta. Vas a tratarlos a ambos como números, y eso es todo lo que te importa. Entonces sum_of_array devuelve el tipo de número. De esto se trata el polimorfismo.

Entonces, hay algunas prácticas que podría estar violando si su función puede devolver múltiples tipos. Conocerlos te ayudará a determinar si tu función particular debería devolver múltiples tipos de todas formas.

    
respondido por el Travis Wilson 27.01.2014 - 23:52
4

En realidad, no es infrecuente en absoluto devolver tipos diferentes incluso en un lenguaje tipificado estáticamente. Es por eso que tenemos tipos de unión, por ejemplo.

De hecho, los métodos en Java casi siempre devuelven uno de los cuatro tipos: algún tipo de objeto o null o una excepción o nunca regresan.

En muchos idiomas, las condiciones de error se modelan como subrutinas que generan un tipo de resultado o un tipo de error. Por ejemplo, en Scala:

def transferMoney(amount: Decimal): Either[String, Decimal]

Este es un ejemplo estúpido, por supuesto. El tipo de retorno significa "devuelve una cadena o un decimal". Por convención, el tipo de la izquierda es el tipo de error (en este caso, una cadena con el mensaje de error) y el tipo de la derecha es el tipo de resultado.

Esto es similar a las excepciones, excepto por el hecho de que las excepciones también son construcciones de flujo de control. De hecho, son equivalentes en poder expresivo a GOTO .

    
respondido por el Jörg W Mittag 28.01.2014 - 16:49
4

Ninguna respuesta ha mencionado los principios de SOLID. En particular, debe seguir el principio de sustitución de Liskov, que cualquier clase que reciba un tipo diferente al esperado puede funcionar con lo que sea que obtenga, sin hacer nada para probar qué tipo se devuelve.

Entonces, si arrojas algunas propiedades adicionales a un objeto, o envuelves una función devuelta con algún tipo de decorador que aún cumple con lo que la función original estaba destinada a cumplir, eres bueno, siempre y cuando ningún código llame a tu función Se basa en este comportamiento, en cualquier ruta de código.

En lugar de devolver una cadena o un entero, un mejor ejemplo podría ser que devuelva un sistema de rociadores o un gato. Esto está bien si todo el código de llamada que va a hacer es llamar a functionInQuestion.hiss (). De hecho, tiene una interfaz implícita que el código de llamada está esperando, y un lenguaje de tipo dinámico no lo obligará a hacer que la interfaz sea explícita.

Lamentablemente, es probable que tus compañeros de trabajo lo hagan, por lo que probablemente tengas que hacer el mismo trabajo en tu documentación, excepto que no hay una manera universalmente aceptada, concisa y analizable de la máquina, como ocurre cuando define una interfaz en un idioma que los tiene.

    
respondido por el psr 28.01.2014 - 19:21
3

El único lugar donde me veo enviando diferentes tipos es para entradas no válidas o "excepciones de los pobres" donde la condición "excepcional" no es muy excepcional. Por ejemplo, desde mi repositorio de funciones de la utilidad PHP este ejemplo resumido:

function ensure_fields($consideration)
{
        $args = func_get_args();
        foreach ( $args as $a ) {
                if ( !is_string($a) ) {
                        return NULL;
                }
                if ( !isset($consideration[$a]) || $consideration[$a]=='' ) {
                        return FALSE;
                }
        }

        return TRUE;
}

La función nominalmente devuelve un BOOLEAN, sin embargo, devuelve NULL en una entrada no válida. Tenga en cuenta que desde PHP 5.3, todas las funciones internas de PHP también se comportan de esta manera . Además, algunas funciones internas de PHP devuelven FALSE o INT en la entrada nominal, vea:

strpos('Hello', 'e');  // Returns INT(1)
strpos('Hello', 'q');  // Returns BOOL(FALSE)
    
respondido por el dotancohen 28.01.2014 - 08:23
3

¡No creo que esto sea una mala idea! En contraste con esta opinión más común, y como ya lo señaló Robert Harvey, los lenguajes de tipo estático como Java introdujeron los genéricos exactamente para situaciones como la que está preguntando. En realidad, Java intenta mantener (siempre que sea posible) la seguridad de los tipos en tiempo de compilación, y en ocasiones los genéricos evitan la duplicación de código, ¿por qué? Porque puede escribir el mismo método o la misma clase que maneja / devuelve diferentes tipos . Voy a hacer un ejemplo muy breve para mostrar esta idea:

Java 1.4

public static Boolean getBoolean(String property){
    return (Boolean) properties.getProperty(property);
}
public static Integer getInt(String property){
    return (Integer) properties.getProperty(property);
}

Java 1.5+

public static <T> getValue(String property, Class<T> clazz) throws WhateverCheckedException{
    return clazz.getConstructor(String.class).newInstance(properties.getProperty(property));
}
//the call will be
Boolean b = getValue("useProxy",Boolean.class);
Integer b = getValue("proxyPort",Integer.class);

En un lenguaje de tipo dinámico, ya que no tiene seguridad de tipos en el momento de la compilación, es absolutamente libre de escribir el mismo código que funciona para muchos tipos. Ya que incluso en un lenguaje estáticamente tipificado se introdujeron genéricos para resolver este problema, es evidente que no es una mala idea escribir una función que devuelva diferentes tipos en un lenguaje dinámico.

    
respondido por el thermz 28.01.2014 - 10:57
2

El desarrollo de software es básicamente un arte y un arte de gestionar la complejidad. Intenta restringir el sistema en los puntos que puede pagar y limitar las opciones en otros puntos. La interfaz de la función es un contrato que ayuda a administrar la complejidad del código al limitar el conocimiento requerido para trabajar con cualquier pieza de código. Al devolver diferentes tipos, expandes significativamente la interfaz de la función agregando a ella todas las interfaces de diferentes tipos que devuelves Y agregando reglas no obvias en cuanto a la interfaz que se devuelve.     

respondido por el Kaerber 28.01.2014 - 09:35
1

Perl lo usa mucho porque lo que hace una función depende de su contexto . Por ejemplo, una función podría devolver una matriz si se usa en un contexto de lista, o la longitud de la matriz si se usa en un lugar donde se espera un valor escalar. Desde el tutorial que es el primer hit para "perl context" , si lo hace:

my @now = localtime();

Entonces @now es una variable de matriz (eso es lo que significa @), y contendrá una matriz como (40, 51, 20, 9, 0, 109, 5, 8, 0).

Si, por el contrario, llama a la función de una manera en la que su resultado tendrá que ser un escalar, con ($ variables son escalares):

my $now = localtime();

entonces hace algo completamente diferente: $ ahora será algo así como "Vie 9 de enero 20:51:40 2009".

Otro ejemplo en el que puedo pensar es en la implementación de API REST, donde el formato de lo que se devuelve depende de lo que el cliente quiere. E.g, HTML, o JSON, o XML. Aunque técnicamente son todos flujos de bytes, la idea es similar.

    
respondido por el RemcoGerlich 28.01.2014 - 11:27
1

En la tierra dinámica, todo se trata de escribir pato. Lo más importante que debe hacer el público expuesto / expuesto es envolver tipos potencialmente diferentes en un contenedor que les brinde la misma interfaz.

function ThingyWrapper(thingy){ //a function constructor (class-like thingy)

    //thingy is effectively private and persistent for ThingyWrapper instances

    if(typeof thingy === 'array'){
        this.alertItems = function(){
            thingy.forEach(function(el){ alert(el); });
        }
    }
    else {
        this.alertItems = function(){
            for(var x in thingy){ alert(thingy[x]); }
        }
    }
}

function gimmeThingy(){
    var
        coinToss = Math.round( Math.random() ),//gives me 0 or 1
        arrayThingy = [1,2,3],
        objectThingy = { item1:1, item2:2, item3:3 }
    ;

    //0 dynamically evaluates to false in JS
    return new ThingyWrapper( coinToss ? arrayThingy : objectThingy );
}

gimmeThingy().alertItems(); //should be same every time except order of numbers - maybe

En ocasiones, podría tener sentido eliminar los diferentes tipos sin un envoltorio genérico, pero honestamente, en los 7 años de escritura de JS, no es algo que haya sido razonable o conveniente hacer muy a menudo. Sobre todo eso es algo que haría en el contexto de entornos cerrados como el interior de un objeto donde las cosas se ponen juntas. Pero no es algo que haya hecho con la frecuencia suficiente para que se me ocurra algún ejemplo.

Sobre todo, te aconsejaría que dejes de pensar en los tipos tanto como en total. Trata con los tipos cuando lo necesita en un lenguaje dinámico. No más. Es todo el punto. No verifique el tipo de cada argumento. Eso es algo que solo me sentiría tentado a hacer en un entorno donde el mismo método podría dar resultados inconsistentes de manera no obvia (así que definitivamente no hagas algo así). Pero no es el tipo lo que importa, es cómo funciona lo que me das.

    
respondido por el Erik Reppen 28.01.2014 - 18:24

Lea otras preguntas en las etiquetas