Existe uma razão pela qual as funções na maioria das linguagens de programação (?) São projetadas para suportar qualquer número de parâmetros de entrada, mas apenas um valor de retorno?
Na maioria dos idiomas, é possível "contornar" essa limitação, por exemplo, usando parâmetros externos, retornando ponteiros ou definindo / retornando estruturas / classes. Mas parece estranho que as linguagens de programação não tenham sido projetadas para suportar múltiplos valores de retorno de uma maneira mais "natural".
Há uma explicação para isso?
def f(): return (1,2,3)
e então você pode usar para-desembalar tupla "split" da tupla:a,b,c = f() #a=1,b=2,c=3
. Não há necessidade de criar uma matriz e extrair elementos manualmente, não há necessidade de definir nenhuma nova classe.[a, b] = f()
vs.[a, b, c] = f()
) e obtidof
por dentronargout
. Eu não sou um grande fã do Matlab, mas isso às vezes é bastante útil.Respostas:
Algumas linguagens, como Python , suportam vários valores de retorno nativamente, enquanto algumas linguagens como C # os suportam por meio de suas bibliotecas base.
Mas, em geral, mesmo em idiomas que os suportam, vários valores de retorno não são usados com frequência porque são desleixados:
É fácil confundir a ordem dos valores de retorno
(Por esse mesmo motivo, muitas pessoas evitam ter muitos parâmetros para uma função; alguns chegam a dizer que uma função nunca deve ter dois parâmetros do mesmo tipo!)
Eles são mais tipificados, mantêm os valores de retorno agrupados como uma unidade lógica e mantêm os nomes de (propriedades de) os valores de retorno consistentes em todos os usos.
O local em que eles são bastante convenientes é em linguagens (como Python), onde vários valores de retorno de uma função podem ser usados como vários parâmetros de entrada para outra. Mas, os casos de uso em que esse é um design melhor do que o uso de uma classe são muito pequenos.
fonte
()
. Isso é uma coisa ou zero? Pessoalmente, eu diria que é uma coisa. Eu posso atribuirx = ()
muito bem, assim como eu posso atribuirx = randomTuple()
. No último, se a tupla retornada estiver vazia ou não, ainda posso atribuir a tupla retornadax
.Porque funções são construções matemáticas que executam um cálculo e retornam um resultado. De fato, muito do que está "oculto" de poucas linguagens de programação se concentra apenas em uma entrada e uma saída, com várias entradas sendo apenas um invólucro fino em torno da entrada - e quando uma única saída de valor não funciona, usando uma única estrutura coesa (ou tupla, ou
Maybe
) seja a saída (embora esse valor de retorno "único" seja composto por muitos valores).Isso não mudou porque os programadores descobriram que os
out
parâmetros são construções estranhas que são úteis apenas em um conjunto limitado de cenários. Como em muitas outras coisas, o suporte não existe porque a necessidade / demanda não existe.fonte
Em matemática, uma função "bem definida" é aquela em que há apenas 1 saída para uma determinada entrada (como uma observação lateral, você pode ter apenas funções de entrada únicas e ainda obter semântica várias entradas usando o curry ).
Para funções com vários valores (por exemplo, raiz quadrada de um número inteiro positivo, por exemplo), é suficiente retornar uma coleção ou sequência de valores.
Para os tipos de funções de que você está falando (funções que retornam vários valores, de tipos diferentes ), eu vejo isso um pouco diferente do que você parece: Eu vejo a necessidade / uso de parâmetros fora como uma solução alternativa para um melhor design ou um estrutura de dados mais útil. Por exemplo, eu preferiria que os
*.TryParse(...)
métodos retornassem umaMaybe<T>
mônada em vez de usar um parâmetro out. Pense neste código em F #:O suporte ao compilador / IDE / análise é muito bom para essas construções. Isso resolveria grande parte da "necessidade" de parâmetros. Para ser completamente honesto, não consigo pensar em nenhum outro método em que essa não seja a solução.
Para outros cenários - os que não me lembro - basta uma simples tupla.
fonte
var (value, success) = ParseInt("foo");
qual seria o tipo de tempo de compilação verificado porque(int, bool) ParseInt(string s) { }
foi declarado. Eu sei que isso pode ser feito com genéricos, mas ainda assim seria uma adição agradável ao idioma.Além do que já foi dito quando você olha para os paradigmas usados na montagem quando uma função retorna, ela deixa um ponteiro para o objeto retornado em um registro específico. Se eles usassem registros variáveis / múltiplos, a função de chamada não saberia onde obter os valores retornados se essa função estivesse em uma biblioteca. Portanto, isso tornaria difícil o vínculo com as bibliotecas e, em vez de definir um número arbitrário de ponteiros retornáveis, eles foram incluídos. Idiomas de nível superior não têm a mesma desculpa.
fonte
Muitos dos casos de uso em que você usaria vários valores de retorno no passado simplesmente não são mais necessários com os recursos da linguagem moderna. Deseja retornar um código de erro? Lance uma exceção ou retorne um
Either<T, Throwable>
. Deseja retornar um resultado opcional? Retornar umOption<T>
. Deseja retornar um dos vários tipos? Retornar umaEither<T1, T2>
união ou uma etiqueta.E mesmo nos casos em que você realmente precisa retornar vários valores, os idiomas modernos geralmente oferecem suporte a tuplas ou algum tipo de estrutura de dados (lista, matriz, dicionário) ou objetos, bem como alguma forma de ligação destrutiva ou correspondência de padrões, o que compõe o empacotamento seus vários valores em um único valor e, em seguida, destruí-lo novamente em vários valores triviais.
Aqui estão alguns exemplos de idiomas que não oferecem suporte ao retorno de vários valores. Realmente não vejo como a adição de suporte a vários valores de retorno os tornaria significativamente mais expressivos para compensar o custo de um novo recurso de idioma.
Rubi
Pitão
Scala
Haskell
Perl6
fonte
sort
função Matlab :sorted = sort(array)
retorna apenas a matriz classificada, enquanto[sorted, indices] = sort(array)
retorna ambos. A única maneira de pensar em Python seria passar uma flag aosort
longo das linhas desort(array, nout=2)
orsort(array, indices=True)
.[a, b, c] = func(some, thing)
) e agir de acordo. Isso é útil, por exemplo, se o cálculo do primeiro argumento de saída for barato, mas o cálculo do segundo for caro. Não estou familiarizado com nenhum outro idioma em que o equivalente ao Matlabsnargout
esteja disponível em tempo de execução.sorted, _ = sort(array)
.sort
função pode dizer que não precisa calcular os índices? Isso é legal, eu não sabia disso.A verdadeira razão pela qual um único valor de retorno é tão popular são as expressões usadas em muitos idiomas. Em qualquer idioma em que você possa ter uma expressão como
x + 1
você já está pensando em termos de valores de retorno únicos, porque você avalia uma expressão em sua cabeça dividindo-a em pedaços e decidindo o valor de cada peça. Você olhax
e decide que seu valor é 3 (por exemplo), e você olha para 1 e depois olha parax + 1
e junte tudo para decidir que o valor do todo é 4. Cada parte sintática da expressão tem um valor, não qualquer outro número de valores; essa é a semântica natural das expressões que todos esperam. Mesmo quando uma função retorna um par de valores, ainda está realmente retornando um valor que está cumprindo dois valores, porque a idéia de uma função que retorna dois valores que não estão de alguma forma agrupados em uma única coleção é muito estranha.As pessoas não querem lidar com a semântica alternativa que seria necessária para que as funções retornassem mais de um valor. Por exemplo, em uma linguagem baseada em pilha como Forth, você pode ter qualquer número de valores de retorno, porque cada função simplesmente modifica a parte superior da pilha, acionando entradas e pressionando as saídas à vontade. É por isso que Forth não tem o tipo de expressão que as línguas normais têm.
Perl é outra linguagem que às vezes pode agir como se as funções estivessem retornando vários valores, mesmo que seja considerado apenas o retorno de uma lista. A maneira como as listas "interpolam" no Perl nos fornece listas como as
(1, foo(), 3)
que podem ter 3 elementos, como a maioria das pessoas que não sabem que o Perl esperaria, mas poderiam facilmente ter apenas 2 elementos, 4 elementos ou qualquer número maior de elementos, dependendofoo()
. As listas no Perl são achatadas para que uma lista sintática nem sempre tenha a semântica de uma lista; pode ser apenas um pedaço de uma lista maior.Outra maneira de fazer com que as funções retornem vários valores seria ter uma semântica de expressão alternativa, em que qualquer expressão pode ter vários valores e cada valor representa uma possibilidade. Tome
x + 1
novamente, mas desta vez imagine quex
tem dois valores {3, 4}, então os valores dex + 1
seriam {4, 5} e os valores dex + x
seriam {6, 8} ou talvez {6, 7, 8} , dependendo se uma avaliação tem permissão para usar vários valores parax
. Uma linguagem como essa pode ser implementada usando backtracking, da mesma forma que o Prolog usa para fornecer várias respostas a uma consulta.Em resumo, uma chamada de função é uma única unidade sintática e uma única unidade sintática tem um valor único na expressão semântica que todos conhecemos e amamos. Qualquer outra semântica o forçaria a maneiras estranhas de fazer coisas, como Perl, Prolog ou Forth.
fonte
Como sugerido nesta resposta , é uma questão de suporte de hardware, embora a tradição em design de linguagem também tenha um papel.
Das três primeiras línguas, Fortran, Lisp e COBOL, a primeira utilizou um único valor de retorno conforme modelado em matemática. O segundo retornou um número arbitrário de parâmetros da mesma maneira que os recebeu: como uma lista (também se podia argumentar que apenas passava e retornava um único parâmetro: o endereço da lista). O terceiro retorna zero ou um valor.
Essas primeiras línguas influenciaram muito o design das línguas que as seguiram, embora a única que retornasse vários valores, Lisp, nunca tenha ganhado muita popularidade.
Quando o C chegou, apesar de influenciado pelas linguagens anteriores, ele enfatizou o uso eficiente de recursos de hardware, mantendo uma estreita associação entre o que a linguagem C fazia e o código da máquina que a implementava. Alguns de seus recursos mais antigos, como variáveis "auto" vs "register", são resultado dessa filosofia de design.
Deve-se também salientar que a linguagem assembly era amplamente popular até os anos 80, quando finalmente começou a ser eliminada do desenvolvimento convencional. As pessoas que escreviam compiladores e criavam idiomas estavam familiarizadas com o assembly e, na maioria das vezes, mantinham o que funcionava melhor lá.
A maioria das línguas que divergiu dessa norma nunca encontrou muita popularidade e, portanto, nunca teve um papel forte influenciando as decisões dos designers de linguagem (que, é claro, foram inspirados pelo que sabiam).
Então, vamos examinar a linguagem assembly. Vejamos primeiro o 6502 , um microprocessador de 1975 que foi famoso pelos microcomputadores Apple II e VIC-20. Era muito fraco em comparação com o que era usado no mainframe e nos minicomputadores da época, embora poderoso comparado aos primeiros computadores de 20, 30 anos antes, no início das linguagens de programação.
Se você olhar para a descrição técnica, ela possui 5 registros e alguns sinalizadores de um bit. O único registro "completo" foi o contador de programas (PC) - esse registro aponta para a próxima instrução a ser executada. Os outros registram onde o acumulador (A), dois "índices" registram (X e Y) e um ponteiro de pilha (SP).
Chamar uma sub-rotina coloca o PC na memória apontada pelo SP e, em seguida, diminui o SP. Retornar de uma sub-rotina funciona em sentido inverso. É possível empurrar e puxar outros valores na pilha, mas é difícil fazer referência à memória em relação ao SP, portanto, escrever sub-rotinas reentrantes foi difícil. Essa coisa que tomamos como certa, chamar uma sub-rotina a qualquer momento que acharmos conveniente, não era tão comum nessa arquitetura. Freqüentemente, uma "pilha" separada seria criada para que os parâmetros e o endereço de retorno da sub-rotina fossem mantidos separados.
Se você olhar para o processador que inspirou o 6502, o 6800 , ele tinha um registro adicional, o Index Register (IX), tão amplo quanto o SP, que poderia receber o valor do SP.
Na máquina, chamar uma sub-rotina reentrante consistia em empurrar os parâmetros na pilha, empurrar PC, mudar PC para o novo endereço e, em seguida, a sub-rotina empurrava suas variáveis locais na pilha . Como o número de variáveis e parâmetros locais é conhecido, é possível resolvê-los em relação à pilha. Por exemplo, uma função que recebe dois parâmetros e possui duas variáveis locais seria assim:
Pode ser chamado várias vezes porque todo o espaço temporário está na pilha.
O 8080 , usado no TRS-80 e em vários microcomputadores baseados em CP / M, poderia fazer algo semelhante ao 6800, pressionando SP na pilha e colocando-a em seu registro indireto, HL.
Essa é uma maneira muito comum de implementar as coisas e tem ainda mais suporte em processadores mais modernos, com o Ponteiro Base que facilita o despejo de todas as variáveis locais antes de retornar com facilidade.
O problema é como você retorna alguma coisa ? Os registros do processador não eram muito numerosos desde o início, e era necessário usar alguns deles até para descobrir qual pedaço de memória endereçar. Devolver as coisas na pilha seria complicado: você teria que estourar tudo, salvar o PC, pressionar os parâmetros de retorno (que seriam armazenados no local enquanto isso?), Depois pressionar o PC novamente e retornar.
Então, o que geralmente era feito era reservar um registro para o valor de retorno. O código de chamada sabia que o valor de retorno estaria em um registro específico, que teria que ser preservado até que pudesse ser salvo ou usado.
Vejamos uma linguagem que permite vários valores de retorno: adiante. O que a Forth faz é manter uma pilha de retorno separada (RP) e uma pilha de dados (SP), de modo que tudo que uma função precisava fazer era popular todos os seus parâmetros e deixar os valores de retorno na pilha. Como a pilha de retorno estava separada, ela não atrapalhou.
Como alguém que aprendeu a linguagem assembly e a Forth nos primeiros seis meses de experiência com computadores, vários valores de retorno parecem totalmente normais para mim. Operadores como o Forth's
/mod
, que retornam a divisão inteira e o resto, parecem óbvios. Por outro lado, eu posso ver facilmente como alguém cuja experiência inicial era C acha esse conceito estranho: vai contra suas expectativas arraigadas sobre o que é uma "função".Quanto à matemática ... bem, eu estava programando computadores muito antes de chegar a funções nas aulas de matemática. Não é uma seção inteira de CS e linguagens de programação que é influenciado pela matemática, mas, em seguida, novamente, há uma seção inteira que não é.
Portanto, temos uma confluência de fatores em que a matemática influenciou o design de linguagem inicial, onde as restrições de hardware ditavam o que era facilmente implementado e onde as linguagens populares influenciavam a evolução do hardware (a máquina Lisp e os processadores de máquina Forth foram atropelamentos nesse processo).
fonte
As linguagens funcionais que eu conheço podem retornar vários valores facilmente através do uso de tuplas (em idiomas de tipo dinâmico, você pode até usar listas). Tuplas também são suportadas em outros idiomas:
No exemplo acima,
f
é uma função retornando 2 ints.Da mesma forma, ML, Haskell, F #, etc., também podem retornar estruturas de dados (os ponteiros são de nível muito baixo para a maioria dos idiomas). Eu não ouvi falar de uma linguagem GP moderna com essa restrição:
Finalmente, os
out
parâmetros podem ser emulados mesmo em linguagens funcionais porIORef
. Há várias razões pelas quais não há suporte nativo para variáveis externas na maioria dos idiomas:Semântica pouco clara : A função a seguir imprime 0 ou 1? Conheço idiomas que imprimem 0 e idiomas que imprimem 1. Existem benefícios para os dois (tanto em termos de desempenho quanto em relação ao modelo mental do programador):
Efeitos não localizados : como no exemplo acima, você pode descobrir que pode ter uma cadeia longa e a função mais interna afeta o estado global. Em geral, fica mais difícil argumentar sobre quais são os requisitos da função e se a alteração é legal. Dado que a maioria dos paradigmas modernos tenta localizar os efeitos (encapsulamento no POO) ou eliminar os efeitos colaterais (programação funcional), entra em conflito com esses paradigmas.
Ser redundante : se você possui tuplas, possui 99% da funcionalidade dos
out
parâmetros e 100% de uso idiomático. Se você adicionar ponteiros à mistura, cobrirá os 1% restantes.Tenho problemas para nomear um idioma que não pôde retornar vários valores usando uma tupla, classe ou
out
parâmetro (e na maioria dos casos, dois ou mais desses métodos são permitidos).fonte
Eu acho que é por causa de expressões como
(a + b[i]) * c
.Expressões são compostas de valores "singulares". Uma função retornando um valor singular pode, portanto, ser usada diretamente em uma expressão, no lugar de qualquer uma das quatro variáveis mostradas acima. Uma função de saída múltipla é pelo menos um pouco desajeitada em uma expressão.
Pessoalmente, sinto que esta é a coisa que é especial sobre um valor de retorno singular. Você pode contornar isso adicionando sintaxe para especificar quais dos vários valores de retorno você deseja usar em uma expressão, mas é mais desajeitado que a boa e velha notação matemática, que é concisa e familiar a todos.
fonte
Isso complica um pouco a sintaxe, mas não há uma boa razão no nível de implementação para não permitir. Ao contrário de algumas das outras respostas, o retorno de vários valores, quando disponível, leva a um código mais claro e eficiente. Não posso contar quantas vezes desejei poder retornar um X e um Y, ou um booleano "sucesso" e um valor útil.
fonte
[out]
parâmetro, mas praticamente todas retornam umHRESULT
(código de erro). Seria bastante prático conseguir um par lá. Em linguagens com bom suporte para tuplas, como Python, isso é usado em muitos códigos que eu já vi.sort
função normalmente ordena um array:sorted_array = sort(array)
. Às vezes eu também preciso dos índices correspondentes:[sorted_array, indices] = sort(array)
. Às vezes, eu só quero que os índices:[~, indices]
= sort (array). The function
sort` possam realmente dizer quantos argumentos de saída são necessários; portanto, se trabalho adicional for necessário para 2 saídas em comparação com 1, ele poderá calcular essas saídas apenas se necessário.Na maioria dos idiomas em que as funções são suportadas, você pode usar uma chamada de função em qualquer lugar onde uma variável desse tipo possa ser usada: -
Se a função retornar mais de um valor, isso não funcionará. Linguagens tipadas dinamicamente, como python, permitem que você faça isso, mas, na maioria dos casos, gera um erro em tempo de execução, a menos que possa descobrir algo sensato a fazer com uma tupla no meio de uma equação.
fonte
foo()[0]
Eu só quero aproveitar a resposta de Harvey. Originalmente, encontrei essa pergunta em um site de notícias tecnológicas (arstechnica) e encontrei uma explicação incrível: sinto que realmente responde ao cerne dessa questão e está ausente de todas as outras respostas (exceto a de Harvey):
A origem do retorno único das funções está no código da máquina. No nível do código da máquina, uma função pode retornar um valor no registro A (acumulador). Quaisquer outros valores de retorno estarão na pilha.
Uma linguagem que suporte dois valores de retorno o compilará como código de máquina que retorna um e coloca o segundo na pilha. Em outras palavras, o segundo valor de retorno acabaria como um parâmetro out de qualquer maneira.
É como perguntar por que a atribuição é uma variável de cada vez. Você pode ter um idioma que permita a, b = 1, 2 por exemplo. Mas isso acabaria no nível do código da máquina sendo a = 1 seguido por b = 2.
Há alguma justificativa para que as construções da linguagem de programação tenham alguma aparência do que realmente acontecerá quando o código for compilado e em execução.
fonte
Tudo começou com a matemática. FORTRAN, nomeado para "Formula Translation" foi o primeiro compilador. FORTRAN foi e é orientado para física / matemática / engenharia.
COBOL, quase tão antigo, não tinha valor de retorno explícito; Mal tinha sub-rotinas. Desde então, tem sido principalmente inércia.
Go , por exemplo, possui vários valores de retorno e o resultado é mais limpo e menos ambíguo do que usar parâmetros "out". Depois de um pouco de uso, é muito natural e eficiente. Eu recomendo que vários valores de retorno sejam considerados para todos os novos idiomas. Talvez para idiomas antigos também.
fonte
Provavelmente tem mais a ver com o legado de como as chamadas de função são feitas nas instruções da máquina processadora e com o fato de que todas as linguagens de programação derivam do código da máquina: por exemplo, C -> Montagem -> Máquina.
Como os processadores executam chamadas de função
Os primeiros programas foram escritos em código de máquina e posteriormente montados. Os processadores suportaram chamadas de função, enviando uma cópia de todos os registros atuais para a pilha. Retornar da função exibirá o conjunto salvo de registros da pilha. Normalmente, um registro foi deixado intocado para permitir que a função de retorno retorne um valor.
Agora, por que os processadores foram projetados dessa maneira ... provavelmente era uma questão de restrições de recursos.
fonte