... ou é apenas uma prática?
Estou perguntando isso por causa de uma discussão com meu professor: perdi o crédito por chamar uma função recursivamente com base no fato de que não cobrimos a recursão em aula, e meu argumento é que aprendemos isso implicitamente por meio de aprendizado return
e métodos.
Estou perguntando aqui porque suspeito que alguém tem uma resposta definitiva.
Por exemplo, qual é a diferença entre os dois métodos a seguir:
public static void a() {
return a();
}
public static void b() {
return a();
}
Além de " a
continuar para sempre" (no programa real, ele é usado corretamente para alertar o usuário novamente quando fornecido com uma entrada inválida), há alguma diferença fundamental entre a
e b
? Para um compilador não otimizado, como eles são tratados de maneira diferente?
Em última análise, tudo se resume a saber se, aprendendo a return a()
partir de b
que também nós mesmos aprendemos a return a()
partir a
. Nós?
a() { do { good = prompt(); } while (!good); }
.Respostas:
Para responder à sua pergunta específica: Não, do ponto de vista de aprendizagem de um idioma, a recursão não é um recurso. Se seu professor realmente reduziu suas notas por usar um "recurso" que ele ainda não havia ensinado, isso estava errado.
Lendo nas entrelinhas, uma possibilidade é que, ao usar a recursão, você evite usar um recurso que deveria ser um resultado de aprendizado para o curso. Por exemplo, talvez você não tenha usado iteração ou talvez tenha usado apenas
for
loops em vez de usar ambosfor
ewhile
. É comum que uma tarefa tenha como objetivo testar sua capacidade de fazer certas coisas e, se você evitar fazê-las, seu professor simplesmente não poderá conceder a você as notas reservadas para esse recurso. No entanto, se essa foi realmente a causa de suas notas perdidas, o professor deve considerar isso como uma experiência de aprendizagem própria - se a demonstração de certos resultados de aprendizagem for um dos critérios para uma tarefa, isso deve ser explicado claramente aos alunos .Dito isso, concordo com a maioria dos outros comentários e respostas de que a iteração é uma escolha melhor do que a recursão aqui. Existem algumas razões e, embora outras pessoas as tenham tocado em alguma extensão, não tenho certeza se elas explicaram completamente o que está por trás delas.
Stack Overflows
O mais óbvio é que você corre o risco de receber um erro de estouro de pilha. Realisticamente, o método que você escreveu é muito improvável de realmente levar a um, uma vez que um usuário teria que fornecer uma entrada incorreta muitas vezes para realmente acionar um estouro de pilha.
No entanto, uma coisa a ter em mente é que não apenas o método em si, mas outros métodos superiores ou inferiores na cadeia de chamadas estarão na pilha. Por causa disso, engolir casualmente o espaço disponível na pilha é uma coisa muito indelicada para qualquer método. Ninguém quer ter que se preocupar constantemente com o espaço livre da pilha sempre que escrever código, devido ao risco de que outro código possa ter usado muito dele desnecessariamente.
Isso faz parte de um princípio mais geral em design de software chamado abstração. Essencialmente, quando você liga
DoThing()
, tudo o que você precisa se preocupar é que a coisa está feita. Você não deve se preocupar com os detalhes de implementação de como isso é feito. Mas o uso ganancioso da pilha quebra esse princípio, porque cada pedaço de código precisa se preocupar com quanta pilha pode assumir com segurança que sobrou para ela por código em outro lugar na cadeia de chamadas.Legibilidade
O outro motivo é a legibilidade. O ideal que o código deve aspirar é ser um documento legível por humanos, onde cada linha descreve simplesmente o que está fazendo. Use estas duas abordagens:
versus
Sim, ambos funcionam e, sim, são muito fáceis de entender. Mas como as duas abordagens podem ser descritas em inglês? Eu acho que seria algo como:
versus
Talvez você possa pensar em uma formulação um pouco menos desajeitada para o último, mas acho que sempre descobrirá que o primeiro será uma descrição mais precisa, conceitualmente, do que você está realmente tentando fazer. Isso não quer dizer que a recursão seja sempre menos legível. Para situações em que ela brilha, como travessia de árvore, você poderia fazer o mesmo tipo de análise lado a lado entre a recursão e outra abordagem e quase certamente descobriria que a recursão fornece um código que é mais claramente autodescritivo, linha por linha.
Isoladamente, ambos são pequenos pontos. É muito improvável que isso realmente leve a um estouro de pilha, e o ganho em legibilidade é mínimo. Mas qualquer programa será uma coleção de muitas dessas pequenas decisões; portanto, mesmo que isoladamente elas não importem muito, é importante aprender os princípios por trás de sua execução.
fonte
Para responder à pergunta literal, ao invés da meta-pergunta: a recursão é uma característica, no sentido de que nem todos os compiladores e / ou linguagens necessariamente permitem isso. Na prática, isso é esperado de todos os compiladores modernos (comuns) - e certamente de todos os compiladores Java! - mas não é universalmente verdadeiro.
Como um exemplo inventado de por que a recursão pode não ser suportada, considere um compilador que armazena o endereço de retorno de uma função em um local estático; pode ser o caso, por exemplo, de um compilador para um microprocessador que não tem uma pilha.
Para tal compilador, quando você chama uma função como esta
é implementado como
e a definição de um (),
é implementado como
Esperançosamente, o problema quando você tenta chamar
a()
recursivamente em tal compilador seja óbvio; o compilador não sabe mais como retornar da chamada externa, porque o endereço de retorno foi sobrescrito.Para o compilador que eu realmente usei (final dos anos 70 ou início dos 80, eu acho) sem suporte para recursão, o problema era um pouco mais sutil do que isso: o endereço de retorno seria armazenado na pilha, assim como nos compiladores modernos, mas as variáveis locais não eram 't. (Teoricamente, isso deveria significar que a recursão era possível para funções sem variáveis locais não estáticas, mas não me lembro se o compilador suportava isso explicitamente ou não. Ele pode ter precisado de variáveis locais implícitas por algum motivo.)
Olhando para o futuro, posso imaginar cenários especializados - sistemas fortemente paralelos, talvez - onde não ter que fornecer uma pilha para cada thread pode ser vantajoso e onde, portanto, a recursão só é permitida se o compilador puder refatorá-la em um loop. (É claro que os compiladores primitivos que discuto acima não eram capazes de tarefas complicadas como refatorar código.)
fonte
O professor quer saber se você estudou ou não. Aparentemente você não resolveu o problema da maneira que ele te ensinou ( o bom caminho ; iteração) e, portanto, considera que não. Sou totalmente a favor de soluções criativas, mas neste caso tenho que concordar com seu professor por um motivo diferente:
se o usuário fornecer entrada inválida muitas vezes (ou seja, mantendo a tecla Enter pressionada), você terá uma exceção de estouro de pilha e seu solução irá falhar. Além disso, a solução iterativa é mais eficiente e fácil de manter. Acho que é por isso que seu professor deveria ter lhe dado.
fonte
Deduzir pontos porque "não cobrimos a recursão na aula" é horrível. Se você aprendeu como chamar a função A, que chama a função B, que chama a função C, que retorna para B, que retorna para A, que retorna para o chamador, e o professor não lhe disse explicitamente que essas funções devem ser diferentes (que seria o caso em versões antigas do FORTRAN, por exemplo), não há razão para que A, B e C não possam ter a mesma função.
Por outro lado, teríamos que ver o código real para decidir se, em seu caso específico, usar a recursão é realmente a coisa certa a fazer. Não há muitos detalhes, mas parece errado.
fonte
Há muitos pontos de vista a serem observados em relação à pergunta específica que você fez, mas o que posso dizer é que, do ponto de vista da aprendizagem de um idioma, a recursão não é uma característica por si só. Se o seu professor realmente reduziu a sua pontuação por usar um "recurso" que ele ainda não havia ensinado, isso estava errado, mas como eu disse, existem outros pontos de vista a serem considerados aqui que na verdade fazem o professor estar certo ao deduzir pontos.
Pelo que posso deduzir de sua pergunta, usar uma função recursiva para pedir entrada em caso de falha de entrada não é uma boa prática, já que todas as chamadas de funções recursivas são colocadas na pilha. Uma vez que essa recursão é orientada pela entrada do usuário, é possível ter uma função recursiva infinita e, portanto, resultando em um StackOverflow.
Não há diferença entre esses 2 exemplos que você mencionou em sua pergunta no sentido do que eles fazem (mas diferem em outras maneiras) - Em ambos os casos, um endereço de retorno e todas as informações de método estão sendo carregados na pilha. Em um caso de recursão, o endereço de retorno é simplesmente a linha logo após a chamada do método (claro que não é exatamente o que você vê no código em si, mas sim no código que o compilador criou). Em Java, C e Python, a recursão é bastante cara em comparação com a iteração (em geral) porque requer a alocação de um novo quadro de pilha. Sem mencionar que você pode obter uma exceção de estouro de pilha se a entrada não for válida muitas vezes.
Acredito que o professor deduziu pontos, já que a recursão é considerada um assunto próprio e é improvável que alguém sem experiência em programação pense em recursão. (Claro que não significa que não vão, mas é improvável).
IMHO, acho que o professor está certo ao deduzir os pontos. Você poderia facilmente ter levado a parte de validação para um método diferente e usá-lo assim:
Se o que você fez pode realmente ser resolvido dessa maneira, então o que você fez foi uma má prática e deve ser evitado.
fonte
hasWon(x, y, piece)
(para verificar apenas a linha e coluna afetadas).Talvez seu professor ainda não tenha ensinado, mas parece que você está pronto para aprender as vantagens e desvantagens da recursão.
A principal vantagem da recursão é que os algoritmos recursivos costumam ser muito mais fáceis e rápidos de escrever.
A principal desvantagem da recursão é que algoritmos recursivos podem causar estouro de pilha, uma vez que cada nível de recursão requer que um quadro de pilha adicional seja adicionado à pilha.
Para código de produção, onde o dimensionamento pode resultar em muitos mais níveis de recursão na produção do que nos testes de unidade do programador, a desvantagem geralmente supera a vantagem e o código recursivo é frequentemente evitado quando prático.
fonte
Em relação à questão específica, a recursão é uma característica, estou inclinado a dizer que sim, mas depois de reinterpretar a questão. Existem opções de design comuns de linguagens e compiladores que tornam a recursão possível, e existem linguagens Turing-completas que não permitem a recursão de forma alguma . Em outras palavras, a recursão é uma habilidade que é habilitada por certas escolhas no design da linguagem / compilador.
O suporte a funções de primeira classe torna a recursão possível sob suposições mínimas; veja um exemplo de loops de escrita em Unlambda , ou esta expressão obtusa do Python que não contém referências, loops ou atribuições:
Linguagens / compiladores que usam vinculação tardia ou que definem declarações futuras tornam a recursão possível. Por exemplo, embora Python permita o código abaixo, isso é uma escolha de design (ligação tardia), não um requisito para um sistema Turing-completo . Funções mutuamente recursivas geralmente dependem do suporte para declarações de encaminhamento.
Linguagens tipadas estaticamente que permitem tipos definidos recursivamente contribuem para habilitar a recursão. Veja esta implementação do Y Combinator em Go . Sem tipos recursivamente definidos, ainda seria possível usar recursão em Go, mas acredito que o combinador Y especificamente seria impossível.
fonte
Pelo que posso deduzir de sua pergunta, usar uma função recursiva para solicitar entrada em caso de falha na entrada não é uma boa prática. Por quê?
Porque toda chamada de função recursiva é colocada na pilha. Uma vez que esta recursão é conduzida pela entrada do usuário, é possível ter uma função recursiva infinita e, portanto, resultando em um StackOverflow :-p
Ter um loop não recursivo para fazer isso é o caminho a percorrer.
fonte
while(true)
chamando o mesmo método? Nesse caso, eu não diria que isso suporta qualquer diferença entre recursão, bom saber como é.while(true)
é um loop infinito. A menos que você tenha umabreak
declaração, não vejo sentido nisso, a menos que você esteja tentando travar seu programa lol. Meu ponto é, se você chamar o mesmo método (que é recursão), às vezes ele vai dar a você um StackOverflowError , mas se você usar um loopwhile
oufor
, não vai. O problema simplesmente não existe com um loop regular. Talvez eu tenha entendido mal, mas minha resposta para você é não.Recursão é um conceito de programação , um recurso (como iteração) e uma prática . Como você pode ver no link, há um grande domínio de pesquisa dedicado ao assunto. Talvez não precisemos nos aprofundar tanto no assunto para entender esses pontos.
Recursão como recurso
Em termos simples, Java o suporta implicitamente, porque permite que um método (que é basicamente uma função especial) tenha "conhecimento" de si mesmo e de outros métodos que compõem a classe a que pertence. Considere uma linguagem em que este não seja o caso: você seria capaz de escrever o corpo desse método
a
, mas não seria capaz de incluir uma chamada paraa
dentro dele. A única solução seria usar iteração para obter o mesmo resultado. Em tal linguagem, você teria que fazer uma distinção entre funções cientes de sua própria existência (usando um token de sintaxe específico) e aquelas que não o fazem! Na verdade, todo um grupo de linguagens faz essa distinção (veja o Lisp e lambdas ) para se chamar recursivamente (novamente, com uma sintaxe dedicada). ML famílias por exemplo). Curiosamente, Perl permite até funções anônimas (as chamadassem recursão?
Para linguagens que nem mesmo suportam a possibilidade de recursão, muitas vezes há outra solução, na forma do combinador de ponto fixo , mas ainda requer que a linguagem suporte funções como os chamados objetos de primeira classe (ou seja, objetos que podem ser manipulado dentro da própria linguagem).
Recursão como prática
Ter esse recurso disponível em um idioma não significa necessariamente que seja idiomático. No Java 8, as expressões lambda foram incluídas, então pode se tornar mais fácil adotar uma abordagem funcional para a programação. No entanto, existem considerações práticas:
O resultado final
Felizmente (ou mais precisamente, para facilidade de uso), Java permite que os métodos reconheçam a si mesmos por padrão e, portanto, suportam a recursão, então este não é realmente um problema prático, mas ainda permanece teórico, e suponho que seu professor queria abordá-lo especificamente. Além disso, à luz da evolução recente da língua, pode se tornar algo importante no futuro.
fonte