É uma má idéia retornar diferentes tipos de dados de uma única função em uma linguagem tipada dinamicamente?

64

Meu idioma principal é datilografado estaticamente (Java). Em Java, você precisa retornar um único tipo de cada método. Por exemplo, você não pode ter um método que condicionalmente retorne um String ou condicionalmente retorne um Integer . Mas em JavaScript, por exemplo, isso é muito possível.

Em uma linguagem com tipagem estática, entendo por que essa é uma má ideia. Se todo método retornou Object (o pai comum de que todas as classes herdam), então você e o compilador não têm idéia do que você está lidando. Você terá que descobrir todos os seus erros em tempo de execução.

Mas em uma linguagem tipada dinamicamente, pode até não haver um compilador. Em uma linguagem tipada dinamicamente, não é óbvio para mim por que uma função que retorna vários tipos é uma má ideia. Meu histórico em linguagens estáticas me impede de escrever essas funções, mas temo estar atento a um recurso que poderia tornar meu código mais limpo de maneiras que não posso ver.

Editar : vou remover meu exemplo (até conseguir pensar em um melhor). Eu acho que é direcionar as pessoas para responder a um ponto que eu não estou tentando fazer.

    
por Daniel Kaplan 27.01.2014 / 18:45
fonte

14 respostas

43

Em contraste com outras respostas, há casos em que é aceitável retornar tipos diferentes.

Exemplo 1

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

Em algumas linguagens estaticamente tipadas, isso envolve sobrecargas, portanto podemos considerar que existem vários métodos, cada um retornando o tipo fixo predefinido. Em linguagens dinâmicas, esta pode ser a mesma função, implementada como:

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

Mesma função, diferentes tipos de valor de retorno.

Exemplo 2

Imagine que você receba uma resposta de um componente OpenID / OAuth. Alguns provedores OpenID / OAuth podem conter mais informações, como a idade da pessoa.

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

Outros teriam o mínimo, seria um endereço de e-mail ou o pseudônimo.

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

Mais uma vez, a mesma função, resultados diferentes.

Aqui, o benefício de retornar tipos diferentes é especialmente importante em um contexto em que você não se importa com tipos e interfaces, mas com os objetos que realmente contêm. Por exemplo, vamos imaginar um site com linguagem madura. Então o findCurrent() pode ser usado assim:

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();
}

Refatorar isso em código, onde cada provedor terá sua própria função, que retornará um tipo fixo bem definido, não apenas degradaria a base de código e causaria a duplicação de código, como também não traria nenhum benefício. Pode-se acabar fazendo horrores como:

var age;
if (['Facebook', 'Yahoo', 'Blogger', 'LiveJournal'].contains(user.provider)) {
    age = user.age;
}
    
por 27.01.2014 / 19:58
fonte
31

Em geral, é uma má idéia pelas mesmas razões que o equivalente moral em uma linguagem tipada estaticamente é uma má idéia: você não tem idéia de qual tipo concreto é retornado, então você não tem idéia do que pode fazer com o resultado ( além das poucas coisas que podem ser feitas com qualquer valor). Em um sistema de tipo estático, você tem anotações de tipos de retorno verificadas pelo compilador, mas em uma linguagem dinâmica o mesmo conhecimento ainda existe - é apenas informal e armazenado em cérebros e documentação, e não em código-fonte.

Em muitos casos, no entanto, há rima e razão para a qual o tipo é retornado e o efeito é semelhante à sobrecarga ou polimorfismo paramétrico em sistemas de tipo estático. Em outras palavras, o tipo de resultado é previsível, não é tão simples de expressar.

Mas note que pode haver outras razões para uma função específica ser mal projetada: Por exemplo, uma função sum retornando false em entradas inválidas é uma má idéia principalmente porque esse valor de retorno é inútil e sujeito a erros (0 < - > confusão falsa).

    
por 27.01.2014 / 19:01
fonte
26

Em linguagens dinâmicas, você não deve perguntar se retorna tipos diferentes , mas objetos com APIs diferentes . Como a maioria das linguagens dinâmicas não se importam com os tipos, mas usam várias versões de digitação de patas .

Ao retornar tipos diferentes fazem sentido

Por exemplo, esse método faz sentido:

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

Porque o arquivo e uma lista de strings são (no Python) iteráveis que retornam string. Tipos muito diferentes, a mesma API (a menos que alguém tente chamar métodos de arquivo em uma lista, mas isso é história diferente).

Você pode retornar condicionalmente list ou tuple ( tuple é uma lista imutável em python).

Formalmente, até mesmo:

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

ou:

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

retorna tipos diferentes, pois tanto o python None quanto o Javascript null são tipos por direito próprio.

Todos esses casos de uso teriam suas contrapartes em idiomas estáticos, a função retornaria apenas uma interface apropriada.

Ao retornar objetos com APIs diferentes condicionalmente, é uma boa ideia

Se o retorno de diferentes APIs é uma boa ideia, na maioria dos casos, o IMO não faz sentido. Somente um exemplo sensato que vem à mente é algo próximo de o que @MainMa disse : quando sua API pode fornecer uma quantidade variável de detalhe pode fazer sentido para retornar mais detalhes, quando disponíveis.

    
por 28.01.2014 / 00:28
fonte
9

Sua pergunta me faz querer chorar um pouco. Não para o uso de exemplo que você forneceu, mas porque alguém, inadvertidamente, levará essa abordagem longe demais. É um pequeno passo de código ridiculamente insustentável.

O caso de uso de condição de erro faz sentido, e o padrão nulo (tudo tem que ser um padrão) em linguagens estaticamente tipadas faz o mesmo tipo de coisa. Sua chamada de função retorna um object ou retorna null .

Mas é um pequeno passo para dizer "Eu vou usar isso para criar um padrão de fábrica " e retornar foo ou bar ou baz dependendo do humor da função . Depurar isso se tornará um pesadelo quando o chamador esperar foo , mas receber bar .

Então eu não acho que você esteja com a mente fechada. Você está sendo cauteloso em como usar os recursos da linguagem.

Divulgação: minha experiência é em idiomas com tipagem estática, e geralmente trabalhei em equipes maiores e diversificadas, onde a necessidade de código sustentável era bastante alta. Então, minha perspectiva provavelmente está inclinada também.

    
por 27.01.2014 / 19:03
fonte
6

O uso de Genéricos em Java permite que você retorne um tipo diferente, mantendo a segurança do tipo estático. Você simplesmente especifica o tipo que deseja retornar no parâmetro de tipo genérico da chamada de função.

Se você poderia usar uma abordagem semelhante em Javascript, é claro, uma questão em aberto. Como o Javascript é uma linguagem digitada dinamicamente, retornar um object parece ser a escolha óbvia.

Se você quiser saber onde um cenário de retorno dinâmico pode funcionar quando estiver acostumado a trabalhar em uma linguagem com tipos estáticos, considere a palavra-chave dynamic em C #. Rob Conery conseguiu com sucesso escrever um mapeador objeto-relacional em 400 linhas de código usando a palavra-chave dynamic .

Naturalmente, todo dynamic realmente faz é envolver uma variável object com alguma segurança de tipo em tempo de execução.

    
por 27.01.2014 / 18:53
fonte
4

É uma má ideia, penso eu, devolver diferentes tipos condicionalmente. Uma das maneiras que isso surge com frequência para mim é se a função puder retornar um ou mais valores. Se apenas um valor precisar ser retornado, pode parecer razoável simplesmente retornar o valor, em vez de empacotá-lo em uma Matriz, para evitar a necessidade de desempacotá-lo na função de chamada. No entanto, isso (e a maioria dos outros exemplos disso) impõe uma obrigação ao chamador de distinguir e manipular os dois tipos. A função será mais fácil de avaliar se estiver sempre retornando o mesmo tipo.

    
por 27.01.2014 / 19:13
fonte
4

A "prática incorreta" existe independentemente de seu idioma ser estaticamente digitado. A linguagem estática faz mais para afastá-lo dessas práticas, e você pode encontrar mais usuários que se queixam de "má prática" em uma linguagem estática, porque é uma linguagem mais formal. No entanto, os problemas subjacentes estão presentes em uma linguagem dinâmica e você pode determinar se eles são justificados.

Aqui está a parte objetável do que você propõe. Se eu não sei que tipo é retornado, então não posso usar o valor de retorno imediatamente. Eu tenho que "descobrir" algo sobre isso.

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

Muitas vezes esse tipo de mudança no código é simplesmente uma má prática. Isso torna o código mais difícil de ler. Neste exemplo, você vê porque jogar e capturar casos de exceção é popular. Dito de outro modo: se sua função não puder fazer o que diz, não deve retornar com sucesso . Se eu chamar sua função, quero fazer isso:

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

Como a primeira linha retorna com sucesso, presumo que total contenha a soma da matriz. Se ele não retornar com sucesso, passamos para outra página do meu programa.

Vamos usar outro exemplo que não é apenas sobre a propagação de erros. Talvez sum_of_array tente ser inteligente e retornar uma string legível em alguns casos, como "Essa é a combinação de meu armário!" se e somente se o array é [11,7,19]. Estou tendo problemas para pensar em um bom exemplo. De qualquer forma, o mesmo problema se aplica. Você precisa inspecionar o valor de retorno antes de fazer qualquer coisa com ele:

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

Você pode argumentar que seria útil para a função retornar um número inteiro ou um ponto flutuante, por exemplo:

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

Mas esses resultados não são tipos diferentes, no que diz respeito a você. Você vai tratar os dois como números, e é só isso que importa. Então, sum_of_array retorna o tipo de número. É disso que trata o polimorfismo.

Portanto, há algumas práticas que você pode estar violando se sua função puder retornar vários tipos. Conhecê-los ajudará a determinar se sua função específica deve retornar vários tipos de qualquer maneira.

    
por 27.01.2014 / 23:52
fonte
4

Na verdade, não é muito incomum para todos retornar tipos diferentes, mesmo em uma linguagem de tipo estaticamente. É por isso que temos tipos de união, por exemplo.

Na verdade, os métodos em Java quase sempre retornam um de quatro tipos: algum tipo de objeto ou null ou uma exceção ou nunca retornam.

Em muitos idiomas, as condições de erro são modeladas como sub-rotinas retornando um tipo de resultado ou um tipo de erro. Por exemplo, em Scala:

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

Este é um exemplo estúpido, é claro. O tipo de retorno significa "retornar uma string ou um decimal". Por convenção, o tipo à esquerda é o tipo de erro (neste caso, uma string com a mensagem de erro) e o tipo correto é o tipo de resultado.

Isso é semelhante a exceções, exceto pelo fato de que as exceções também são construções de fluxo de controle. Eles são, de fato, equivalentes em poder expressivo a GOTO .

    
por 28.01.2014 / 16:49
fonte
4

Nenhuma resposta mencionou os princípios do SOLID. Em particular, você deve seguir o princípio da substituição de Liskov, de que qualquer classe que receba um tipo diferente do esperado pode ainda trabalhar com o que tiver, sem fazer nada para testar qual tipo é retornado.

Então, se você jogar algumas propriedades extras em um objeto, ou quebrar uma função retornada com algum tipo de decorador que ainda realiza o que a função original pretendia realizar, você é bom, contanto que nenhum código chame sua função depende desse comportamento, em qualquer caminho de código.

Em vez de retornar uma string ou um inteiro, um exemplo melhor pode ser um sistema de sprinklers ou um cat. Isso é bom se todo o código de chamada for fazer é chamar functionInQuestion.hiss (). Efetivamente você tem uma interface implícita que o código de chamada está esperando, e uma linguagem tipada dinamicamente não vai forçar você a tornar a interface explícita.

Infelizmente, seus colegas de trabalho provavelmente o farão, então você provavelmente terá que fazer o mesmo trabalho de qualquer maneira em sua documentação, exceto que não há uma maneira universalmente aceita e analisável pela máquina para fazê-lo - como acontece quando você defina uma interface em uma linguagem que os tenha.

    
por 28.01.2014 / 19:21
fonte
3

O único lugar em que me vejo enviando tipos diferentes é por entrada inválida ou "exceções de pobres", em que a condição "excepcional" não é muito excepcional. Por exemplo, do meu repositório de funções do utilitário PHP , este exemplo abreviado:

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;
}

A função nominalmente retorna um BOOLEAN, porém retorna NULL em entrada inválida. Note que desde o PHP 5.3, todas as funções internas do PHP se comportam assim também . Adicionalmente, algumas funções internas do PHP retornam FALSE ou INT na entrada nominal, veja:

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

Eu não acho que seja uma má ideia! Em contraste com essa opinião mais comum, e como já apontado por Robert Harvey, linguagens estaticamente tipificadas como Java introduziram Generics exatamente para situações como a que você está pedindo. Na verdade, o Java tenta manter (sempre que possível) segurança de tipos em tempo de compilação e, às vezes, os Generics evitam a duplicação de código, por quê? Porque você pode escrever o mesmo método ou a mesma classe que manipula / retorna tipos diferentes . Vou fazer apenas um breve exemplo para mostrar essa ideia:

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);

Em uma linguagem tipada dinamicamente, desde que você não tenha nenhuma segurança em tempo de compilação, você está absolutamente livre para escrever o mesmo código que funciona para muitos tipos. Como mesmo em uma linguagem com tipagem estática, o Generics foi introduzido para resolver esse problema, é claramente uma indicação de que escrever uma função que retorna tipos diferentes em uma linguagem dinâmica não é uma má idéia.

    
por 28.01.2014 / 10:57
fonte
2

O desenvolvimento de software é basicamente uma arte e um ofício de gerenciar a complexidade. Você tenta restringir o sistema nos pontos que você pode pagar e limitar as opções em outros pontos. A interface da função é um contrato que ajuda a gerenciar a complexidade do código, limitando o conhecimento necessário para trabalhar com qualquer parte do código. Ao retornar diferentes tipos, você expande significativamente a interface da função, adicionando a ela todas as interfaces de tipos diferentes que você retorna e adicionando regras não óbvias a respeito de qual interface é retornada.

    
por 28.01.2014 / 09:35
fonte
1

O Perl usa muito isso, porque o que uma função faz depende do seu contexto . Por exemplo, uma função poderia retornar um array se usado no contexto de lista, ou o comprimento do array se usado em um lugar onde um valor escalar é esperado. De o tutorial que é o primeiro hit do "contexto perl" , se você fizer isso:

my @now = localtime();

Então @now é uma variável de matriz (é o que significa @) e conterá uma matriz como (40, 51, 20, 9, 0, 109, 5, 8, 0).

Se, ao invés disso, você chamar a função de um modo onde o resultado terá que ser escalar, com ($ variables são escalares):

my $now = localtime();

então faz algo completamente diferente: $ agora será algo como "Sex Jan 9 20:51:40 2009".

Outro exemplo que posso pensar é na implementação de APIs REST, onde o formato do que é retornado depende do que o cliente deseja. Por exemplo, HTML, JSON ou XML. Embora tecnicamente sejam todos fluxos de bytes, a ideia é semelhante.

    
por 28.01.2014 / 11:27
fonte
1

Na terra dinâmica, é tudo sobre digitação de pato. A coisa mais responsável exposta / voltada para o público a fazer é envolver tipos potencialmente diferentes em um wrapper que forneça a mesma interface.

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

Ocasionalmente, pode fazer sentido remover tipos diferentes completamente sem invólucro genérico, mas, honestamente, nos 7 anos em que escrevi JS, não é algo que achei razoável ou conveniente fazer com muita frequência. Principalmente isso é algo que eu faria no contexto de ambientes fechados, como o interior de um objeto, onde as coisas estão sendo montadas. Mas não é algo que eu tenha feito com frequência suficiente para que qualquer exemplo seja lembrado.

Na maioria das vezes, eu aconselho que você pare de pensar sobre os tipos como um todo. Você lida com tipos quando precisa em uma linguagem dinâmica. Não mais. É o ponto inteiro. Não verifique o tipo de cada argumento. Isso é algo que eu só seria tentado a fazer em um ambiente onde o mesmo método poderia dar resultados inconsistentes de maneiras não óbvias (então, definitivamente, não faça algo assim). Mas não é o tipo que importa, é como tudo o que você me dá funciona.

    
por 28.01.2014 / 18:24
fonte