public static void Main(string[] args) { try { Action @do = null; @do = () => { WriteLine("Hellow World!"); @do(); }; @do(); } catch (Exception ex) { ForegroundColor = ConsoleColor.Red; WriteLine(ex); ResetColor(); } WriteLine("Pressione"); ReadLine(); } para continuar.
... se o executarmos como Any CPU, o código estourará StackOverflowException. Note, entretanto, que este tipo de exception não é capturado pelo Try-Catch. Isto corre pois StackOverflowException é uma exceção não gerenciada. Por consequencia ela leva ao encerramento abrupto do processo.
Agora, se executarmos o mesmo código em Release/x64, a exception não ocorre. Há duas razões pelas quais isso acontece:
- A função Do(int) é classificada como um Tail-Call Function
- Ao compilarmos em Release/x64, o compilador inclui otmizações no código para Tail-Call functions, removendo a recursividade.
Vamos então, a efeito de testes, fazer uma modificação no nosso código.
@do = () => { WriteLine("Hellow "); @do(); WriteLine("world!"); }; @do(1);
Ao executar o código agora em Release/x64, novamente é gerado StackOverflowException! E fica a pergunta: Porquê? Porque ao incluirmos o WriteLine, descaracterizamos a função Do como uma Tail-Call Function.
Loops infinitos por si só já são um perigo para as aplicações. Apesar disso, temos reações diferente ao usarmos ou não Tail-Call functions: Num loop infinito, consome-se recursos computacionais. Desta forma, ao usar Tail-Call functions seu processo só vai abendar quando não houver mais memória, ou ainda que a alocação de memória adicional não seja realizada pela função, ainda sim o consumo do processador levará a máquina a instabilidade ou lentidão. Imagine quão devastador seria ter algumas threads no IIS rodando loops infinitos em backgroud! Caso abendasse (no caso de não serem usadas Tail-call functions), estes efeitos colaterais não ocorreriam.
Com stack overflow ou não, a Microsoft recomenta que "devemos escrever nosso código para detectar e prevenir stack overflow. Por exemplo, se sua aplicação depende de recursão, use um contador ou uma condição de estado para finalizar o loop recursivo." (https://msdn.microsoft.com/en-us/library/system.stackoverflowexception)
Escrevendo Tail-Call Functions
Por exemplo, tomemos por exemplo uma função que calcule o fatorial:private static int Fatorial(int i) { if (i == 1) return 1; return i*Fatorial(i - 1); }
Reparem que esta função não é Tail-recursive, pois a ultima operação realizada é multiplicação. Para transformar esta função em uma Tail-Call function nós podemos usar algumastécnicas de programação funcional:
Técnica 1 - Accumulator Passing Style
Esta estratégia basea-se em passar uma variável acumuladora para o método. Poderíamos transformá-la em tail recursive, informando um acumulador no argumento da função.private static int Fatorial(int i, int acc == 1) { if (i == 1) return acc; return Fatorial(i - 1, acc * i); }
Infelizmente esta abordagem acrescenta um argumento não-funcional na nossa função. Reparem que preciso invocá-la passando o argumeto opcional 1. Porém isso pode ser contornado:
private static int Fatorial(int i) { Func<int, int, int> _fatorial = null; _fatorial = (x, acc) => x == 1 ? acc : _fatorial(x - 1, acc*x); return _fatorial(i, 1); }
Técnica 2 - Continuation Passing Style
A segunda estratégia resume-se em passar o ponteiro de uma função que será usado na continuação da rotina. É uma espécie de callback.public static int Fatorial(int n) { Func<int, Func<int, int>, int> _fatorial = null; _fatorial = (_n, continuation) => { return _n == 1 ? continuation(1) : _fatorial(_n - 1, result => continuation(result * _n)); }; return _fatorial(n, result => result); }
Nenhum comentário:
Postar um comentário