Mi idioma principal es de tipo estático (Java). En Java, debe devolver un solo tipo de cada método. Por ejemplo, no puede tener un método que devuelva condicionalmente a String
o devuelva condicionalmente un Integer
. Pero en JavaScript, por ejemplo, esto es muy posible.
En un lenguaje escrito estáticamente, entiendo por qué es una mala idea. Si todos los métodos devueltos Object
(el padre común del que heredan todas las clases), usted y el compilador no tienen idea de lo que están tratando. Tendrás que descubrir todos tus errores en 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 varios tipos es una mala idea. Mi experiencia en lenguajes estáticos hace que evite escribir tales funciones, pero me temo que estoy siendo una mente cerrada acerca de una característica que podría hacer que mi código sea más limpio de una manera que no puedo ver.
Editar : voy a eliminar mi ejemplo (hasta que se me ocurra uno mejor). Creo que está guiando a la gente a responder a un punto que no estoy tratando de hacer.
fuente
(coerce var 'string)
rendimientos de unastring
o(concatenate 'string this that the-other-thing)
de igual manera. También he escrito cosasThingLoader.getThingById (Class<extends FindableThing> klass, long id)
así. Y, allí, podría devolverle algo que subclasifique lo que solicitó:loader.getThingById (SubclassA.class, 14)
podría devolver unSubclassB
SubclassA
Respuestas:
A diferencia de otras respuestas, hay casos en los que es aceptable devolver diferentes tipos.
Ejemplo 1
En algunos lenguajes de tipo estático, 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:
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.
Otros tendrían el mínimo, sería una dirección de correo electrónico o el seudónimo.
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 los objetos que realmente contienen. Por ejemplo, imaginemos que un sitio web contiene lenguaje maduro. Entonces
findCurrent()
se puede usar así:Refactorizar esto en código donde cada proveedor tendrá su propia función que devolverá un tipo fijo bien definido no solo degradaría la base del código y causaría la duplicación del código, sino que tampoco brindaría ningún beneficio. Uno puede terminar haciendo horrores como:
fuente
sum
ejemplo.sum :: Num a => [a] -> a
. Puede sumar una lista de cualquier cosa que sea un número. A diferencia de JavaScript, si intenta sumar algo que no es un número, el error se detectará en el momento de la compilación.Iterator[A]
tiene un métododef sum[B >: A](implicit num: Numeric[B]): B
, que nuevamente permite sumar cualquier tipo de números y se verifica en tiempo de compilación.+
cadenas (implementandoNum
, lo cual es una idea terrible en cuanto al diseño pero legal) o inventar un operador / función diferente que esté sobrecargado por enteros y cadenas respectivamente. Haskell tiene polimorfismo ad-hoc a través de clases de tipos. Una lista que contiene enteros y cadenas es mucho más difícil (posiblemente imposible sin extensiones de lenguaje) pero es un asunto bastante diferente.null
valores.En general, es una mala idea por las mismas razones que el equivalente moral en un lenguaje de tipo 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 pocas cosas que se pueden hacer con cualquier valor). En un sistema de tipo estático, tiene anotaciones verificadas por el compilador de tipos de retorno y similares, pero en un lenguaje dinámico todavía existe el mismo conocimiento: es informal y se almacena en cerebros y documentación en lugar de en el código fuente.
Sin embargo, en muchos casos, hay una rima y una razón para el tipo que se devuelve 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 simple 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
sum
función 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).fuente
NaN
sea "contrapunto" en absoluto. NaN es en realidad una entrada válida para cualquier cálculo de punto flotante. Puedes hacer cualquier cosa con loNaN
que podrías hacer con un número real. Es cierto que el resultado final de dicho cálculo puede no ser muy útil para usted, pero no dará lugar a errores extraños de tiempo de ejecución y no es necesario que lo verifique todo el tiempo ; solo puede verificarlo una vez al final de un serie de cálculos. NaN no es un tipo diferente , es solo un valor especial , algo así como un objeto nulo .NaN
como el objeto nulo, tiende a ocultarse cuando se producen errores, solo para que aparezcan más tarde. Por ejemplo, es probable que su programa explote si seNaN
desliza en un bucle condicional.NaN
valor (a) no es en realidad un tipo de retorno diferente y (b) tiene una semántica de punto flotante bien definida y, por lo tanto, realmente no califica como un análogo a un valor de retorno de tipo variable. En realidad, no es tan diferente de cero en aritmética de enteros; Si un cero "accidentalmente" se desliza en sus cálculos, a menudo es probable que termine con cero como resultado o con un error de división por cero. ¿Los valores de "infinito" definidos por el punto flotante IEEE también son malos?En lenguajes dinámicos, no debe preguntar si devuelve diferentes tipos sino objetos con diferentes API . Como a la mayoría de los lenguajes dinámicos no les importan los tipos, pero usa varias versiones de tipeo de pato .
Cuando regresan diferentes tipos tienen sentido
Por ejemplo, este método tiene sentido:
Porque tanto el archivo como una lista de cadenas son (en Python) iterables que devuelven cadenas. 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).
Puede devolver condicionalmente
list
otuple
(tuple
es una lista inmutable en python).Formalmente incluso haciendo:
o:
devuelve diferentes tipos, ya que python
None
y Javascriptnull
son tipos por derecho propio.Todos estos casos de uso tendrían su contraparte en lenguajes estáticos, la función simplemente devolvería una interfaz adecuada.
Al devolver objetos con diferentes API condicionalmente es una buena idea
En cuanto a si devolver una API diferente es una buena idea, IMO en la mayoría de los casos no tiene sentido. El único ejemplo sensato que viene a la mente es algo parecido a lo que dijo @MainMa : cuando su API puede proporcionar una cantidad variable de detalles, podría tener sentido devolver más detalles cuando estén disponibles.
fuente
do_file
devuelve un iterable de cadenas de cualquier manera.iter()
tanto a la lista como al archivo para asegurarse de que el resultado solo se pueda usar como un iterador en ambos casos.None or something
es un asesino para la optimización del rendimiento realizado por herramientas como PyPy, Numba, Pyston y otras. Este es uno de los Python-ism que hacen que Python no sea tan rápido.Tu pregunta me da ganas de llorar un poco. No para el uso de ejemplo que proporcionó, sino porque alguien inconscientemente llevará este enfoque demasiado lejos. Está a un paso del código ridículamente imposible de mantener.
El caso de uso de la condición de error tiene sentido, y el patrón nulo (todo tiene que ser un patrón) en lenguajes tipados estáticamente hace el mismo tipo de cosas. Su llamada de función devuelve
object
o vuelvenull
.Pero es un pequeño paso para decir "Voy a usar esto para crear un patrón de fábrica " y devolver bien
foo
obar
obaz
dependiendo del estado de ánimo de la función. La depuración de esto se convertirá en una pesadilla cuando la persona que llama esperafoo
pero recibióbar
.Así que no creo que te estén cerrando la mente. Estás siendo cauteloso en cómo usar las funciones del lenguaje.
Divulgación: mi experiencia es en lenguajes tipados estáticamente y, en general, he trabajado en equipos más grandes y diversos donde la necesidad de código mantenible era bastante alta. Por lo tanto, mi perspectiva probablemente también esté sesgada.
fuente
El uso de Generics en Java le permite devolver un tipo diferente, mientras conserva la seguridad del tipo estático. Simplemente especifique el tipo que desea devolver en el parámetro de tipo genérico de la llamada a la función.
Si podría usar un enfoque similar en Javascript es, por supuesto, una pregunta abierta. Dado que Javascript es un lenguaje de tipo dinámico, devolver un
object
parece ser la opción obvia.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 mirar la
dynamic
palabra clave en C #. Rob Conery pudo escribir con éxito un Mapeador Relacional de Objetos en 400 líneas de código usando ladynamic
palabra clave.Por supuesto, todo
dynamic
lo que realmente hace es envolver unaobject
variable con cierta seguridad de tipo de tiempo de ejecución.fuente
Creo que es una mala idea devolver diferentes tipos condicionalmente. Una de las formas en que esto surge con frecuencia para mí es si la función puede devolver uno o más valores. Si solo se necesita devolver un valor, puede parecer razonable simplemente devolver el valor, en lugar de empaquetarlo en una matriz, para evitar tener que desempaquetarlo en la función de llamada. Sin embargo, esta (y la mayoría de las otras instancias de esto) impone una obligación a la persona que llama para distinguir y manejar ambos tipos. Será más fácil razonar sobre la función si siempre devuelve el mismo tipo.
fuente
La "mala práctica" existe independientemente de si su idioma está estáticamente escrito. El lenguaje estático hace más para alejarte de estas prácticas, y puedes 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 ahí 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, entonces no puedo usar el valor de retorno inmediatamente. Tengo que "descubrir" algo al respecto.
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 debe hacer, no debería regresar con éxito . Si llamo a su función, quiero hacer esto:
Debido a que la primera línea regresa con éxito, supongo que en
total
realidad contiene la suma de la matriz. Si no regresa con éxito, saltamos a 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]. Tengo 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:
Podría argumentar que sería útil para la función devolver un número entero o un flotante, por ejemplo:
Pero esos resultados no son diferentes, en lo que a usted respecta. Los tratarás 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 varios tipos. Conocerlos lo ayudará a determinar si su función particular debería devolver múltiples tipos de todos modos.
fuente
En realidad, no es muy raro devolver diferentes tipos incluso en un lenguaje estáticamente tipado. Por eso tenemos tipos de unión, por ejemplo.
De hecho, los métodos en Java casi siempre devuelven uno de cuatro tipos: algún tipo de objeto
null
o una excepción o nunca regresan en absoluto.En muchos idiomas, las condiciones de error se modelan como subrutinas que devuelven un tipo de resultado o un tipo de error. Por ejemplo, en Scala:
Este es un ejemplo estúpido, por supuesto. El tipo de retorno significa "devolver una cadena o un decimal". Por convención, el tipo izquierdo es el tipo de error (en este caso, una cadena con el mensaje de error) y el tipo derecho 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
.fuente
Unit
(no se requiere un método para regresar). Es cierto que en realidad no usa lareturn
palabra clave, pero es un resultado que se obtiene del método. Con excepciones marcadas, incluso se menciona explícitamente en la firma de tipo.GOTO
, en realidad).Ninguna respuesta ha mencionado aún los principios SÓLIDOS. En particular, debe seguir el principio de sustitución de Liskov, de que cualquier clase que reciba un tipo distinto al esperado puede seguir trabajando con lo que sea, sin hacer nada para probar qué tipo se devuelve.
Por lo tanto, si arroja algunas propiedades adicionales sobre un objeto, o envuelve una función devuelta con algún tipo de decorador que aún logra lo que la función original estaba destinada a lograr, es bueno, siempre y cuando ningún código que llame a su función se base en esto comportamiento, en cualquier ruta de código.
En lugar de devolver una cadena o un entero, un mejor ejemplo podría ser que devuelve un sistema de rociadores o un gato. Esto está bien si todo lo que va a hacer el código de llamada es llamar a functionInQuestion.hiss (). Efectivamente, tiene una interfaz implícita que el código de llamada espera, y un lenguaje de tipo dinámico no lo obligará a hacer explícita la interfaz.
Lamentablemente, sus compañeros de trabajo probablemente lo harán, por lo que es probable que tenga que hacer el mismo trabajo de todos modos en su documentación, excepto que no existe una forma universalmente aceptada, analizable y analizable por máquina para hacerlo, como ocurre cuando define una interfaz en un idioma que los tiene.
fuente
El único lugar donde me veo enviando diferentes tipos es para entradas no válidas o "excepciones de hombres pobres" donde la condición "excepcional" no es muy excepcional. Por ejemplo, desde mi repositorio de funciones de utilidad PHP, este ejemplo abreviado:
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 FALSO o INT en la entrada nominal, consulte:
fuente
null
, porque (a) falla alto , por lo que es más probable que se solucione en la fase de desarrollo / prueba, (b) es fácil de manejar, porque puedo detectar todas las excepciones raras / inesperadas una vez por toda mi aplicación (en mi controlador frontal) y solo lo registro (generalmente envío correos electrónicos al equipo de desarrollo), para que pueda solucionarlo más tarde. Y en realidad odio que la biblioteca PHP estándar utilice principalmente el enfoque de "retorno nulo"; realmente solo hace que el código PHP sea más propenso a errores, a menos que verifique todoisset()
, lo que es una carga demasiado pesada.null
un error, por favor, déjelo claro en un docblock (por ejemplo@return boolean|null
), de modo que si encuentro su código algún día no tendría que verificar el cuerpo de la función / método.¡No creo que sea una mala idea! En contraste con esta opinión más común, y como ya señaló Robert Harvey, los lenguajes estáticamente tipados como Java introdujeron genéricos exactamente para situaciones como la que usted está preguntando. En realidad, Java intenta mantener (siempre que sea posible) la seguridad de tipos en el momento de la compilación y, a veces, 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 breve ejemplo para mostrar esta idea:
Java 1.4
Java 1.5+
En un lenguaje de escritura dinámico, dado que no tiene seguridad de escritura en tiempo de compilación, es absolutamente libre de escribir el mismo código que funciona para muchos tipos. Dado que incluso en un lenguaje de tipo estático, se introdujeron genéricos para resolver este problema, es claramente una pista de que escribir una función que devuelve diferentes tipos en un lenguaje dinámico no es una mala idea.
fuente
El desarrollo de software es básicamente un arte y un oficio de gestionar la complejidad. Intenta reducir el sistema en los puntos que puede permitirse y limitar las opciones en otros puntos. La interfaz de la función es un contrato que ayuda a gestionar la complejidad del código al limitar el conocimiento requerido para trabajar con cualquier parte del código. Al devolver diferentes tipos, expande significativamente la interfaz de la función agregando todas las interfaces de diferentes tipos que devuelve Y agregando reglas no obvias sobre qué interfaz se devuelve.
fuente
Perl usa esto 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 el contexto de la lista, o la longitud de la matriz si se usa en un lugar donde se espera un valor escalar. Del tutorial que es el primer éxito para "contexto perl" , si lo hace:
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, en cambio, llama a la función de una manera donde su resultado tendrá que ser un escalar, con ($ las variables son escalares):
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 quiera. Por ejemplo, HTML o JSON o XML. Aunque técnicamente esos son todos flujos de bytes, la idea es similar.
fuente
String
. Ahora las personas solicitan documentación sobre el tipo de devolución. Las herramientas de Java pueden generarlo automáticamente si devuelvo una clase, pero debido a que elegíString
no puedo generar la documentación automáticamente. En retrospectiva / moral de la historia, me gustaría devolver un tipo por método.En la tierra dinámica se trata de tipear patos. La cosa más responsable expuesta / pública es envolver tipos potencialmente diferentes en un contenedor que les da la misma interfaz.
Puede tener sentido ocasionalmente expulsar diferentes tipos sin un envoltorio genérico, pero honestamente en 7 años de escribir JS, no es algo que haya encontrado que sea razonable o conveniente hacer con mucha frecuencia. Principalmente, eso es algo que haría en el contexto de entornos cerrados como el interior de un objeto donde las cosas se están armando. Pero no es algo que haya hecho con tanta frecuencia como para que se me ocurran ejemplos.
Principalmente, te aconsejaría que dejes de pensar en los tipos por completo. Tratas con los tipos cuando lo necesitas en un lenguaje dinámico. No más. Es todo el punto. No verifique el tipo de cada argumento. Eso es algo que solo estaría tentado de hacer en un entorno en el que el mismo método podría dar resultados inconsistentes de maneras no obvias (así que definitivamente no haga algo así). Pero no es el tipo lo que importa, sino cómo funciona lo que sea que me des.
fuente