| Bellacosa Mainframe apresenta o cobol recursivo parte i |
☕ Um Café no Bellacosa Mainframe
COBOL Recursivo — Muito Além do Fatorial: A Engenharia Invisível da Pilha de Execução no IBM Z
"Você provavelmente passará a carreira inteira escrevendo sistemas COBOL sem precisar desenvolver um programa recursivo. Ainda assim, compreender recursividade talvez seja uma das melhores formas de entender como o Enterprise COBOL realmente funciona por dentro."
Introdução
Existe um fenômeno curioso no universo do desenvolvimento Mainframe.
Pergunte para cem programadores COBOL se eles já utilizaram um programa recursivo em produção.
Talvez apenas cinco levantem a mão.
Pergunte agora se eles sabem exatamente como funciona um CALL, como o compilador cria uma nova instância do programa, o que acontece com a Working-Storage, como a pilha de execução (Call Stack) é organizada pelo Language Environment (LE) e por que existe a Local-Storage Section.
Muito provavelmente o número continuará sendo pequeno.
E esse é justamente o paradoxo.
Embora a recursividade seja pouco utilizada nos sistemas corporativos tradicionais, ela revela alguns dos conceitos mais importantes da arquitetura do COBOL moderno.
Ela nos obriga a abandonar a visão simplificada de que um programa é apenas uma sequência de comandos e nos faz enxergar aquilo que realmente está acontecendo:
memória;
pilha de execução;
contexto de chamadas;
passagem de parâmetros;
gerenciamento de variáveis;
criação e destruição de ambientes de execução.
Em outras palavras...
Recursividade não é apenas um algoritmo.
É uma janela para entender como o próprio Enterprise COBOL foi construído.
Prepare seu café.
Hoje vamos abrir a tampa do compilador.
O mito da recursividade em COBOL
Existe uma frase repetida há décadas.
"COBOL não foi feito para recursividade."
Essa afirmação está apenas parcialmente correta.
O COBOL clássico da década de 1960 realmente não possuía suporte adequado para chamadas recursivas.
Naquela época, memória era extremamente cara.
Processadores trabalhavam com poucos kilobytes.
Cada instrução era planejada para consumir o mínimo possível.
O objetivo principal do COBOL era simples:
ler registros;
processar registros;
gravar registros.
Tudo de forma linear.
Imagine um processamento bancário.
READ
↓
VALIDA
↓
CALCULA
↓
UPDATE
↓
WRITE
↓
READ
Milhões de vezes.
Sem árvores.
Sem grafos.
Sem estruturas hierárquicas.
Sem necessidade de chamar o mesmo programa novamente.
A arquitetura inteira do COBOL nasceu voltada para processamento sequencial.
Então por que o COBOL moderno suporta recursividade?
Porque o mundo mudou.
Hoje o Enterprise COBOL conversa diariamente com:
XML
JSON
REST APIs
C
C++
Java
Assembler
Language Environment
Web Services
z/OS Connect
IBM MQ
E todos esses ambientes trabalham intensamente com estruturas hierárquicas.
Uma árvore XML, por exemplo, é naturalmente recursiva.
<empresa>
<departamento>
<funcionario>
<dependente/>
</funcionario>
</departamento>
</empresa>
Cada elemento pode conter outros elementos.
Não existe limite teórico.
A melhor maneira de percorrer isso?
Recursão.
O verdadeiro significado de um programa recursivo
Quando um Padawan ouve falar em recursividade, normalmente pensa:
"É quando um programa chama ele mesmo."
Tecnicamente isso está correto.
Mas essa definição é superficial.
O que realmente acontece é isto:
Cada chamada cria um novo ambiente completo de execução.
Isso muda completamente nossa visão.
Imagine uma rotina simples.
PROCESSA(5)
Ela chama:
PROCESSA(4)
Que chama:
PROCESSA(3)
Que chama:
PROCESSA(2)
Que chama:
PROCESSA(1)
Visualmente:
PROCESSA(5)
↓
PROCESSA(4)
↓
PROCESSA(3)
↓
PROCESSA(2)
↓
PROCESSA(1)
Não existe apenas um programa funcionando.
Existem cinco instâncias simultâneas do mesmo programa.
Cada uma em um estágio diferente da execução.
Esse conceito é fundamental.
A grande mágica acontece na pilha (Stack)
Sempre que uma chamada ocorre, o Language Environment cria um novo frame.
Pense na pilha como uma torre de caixas.
+----------------------+
| PROCESSA(1) |
+----------------------+
| PROCESSA(2) |
+----------------------+
| PROCESSA(3) |
+----------------------+
| PROCESSA(4) |
+----------------------+
| PROCESSA(5) |
+----------------------+
Cada caixa contém:
parâmetros;
registradores;
endereço de retorno;
variáveis locais;
contexto da execução.
Quando PROCESSA(1) termina:
a caixa é removida.
Depois PROCESSA(2).
Depois PROCESSA(3).
Até voltar ao programa original.
Essa estrutura recebe o nome de Call Stack.
Todo programador deveria conhecê-la.
Mesmo que nunca escreva um algoritmo recursivo.
A pilha sempre existiu
Existe outro detalhe curioso.
Mesmo programas totalmente lineares utilizam pilha.
Por exemplo.
Programa A.
CALL B
Programa B.
CALL C
Programa C.
CALL D
Visualmente:
A
↓
B
↓
C
↓
D
O sistema operacional precisa lembrar para onde voltar.
Então cria uma pilha.
+-----------+
| D |
+-----------+
| C |
+-----------+
| B |
+-----------+
| A |
+-----------+
Perceba algo interessante.
A recursividade apenas repete esse processo.
A diferença é que quem aparece novamente é o mesmo programa.
O compilador não tem medo disso
Muitos imaginam que o compilador "fica confuso".
Na realidade...
Para ele não faz diferença.
Ele apenas cria outra instância.
Depois outra.
Depois outra.
Até encontrar a condição de parada.
O maior perigo da recursividade
Imagine o seguinte pseudocódigo.
PROCESSA
↓
PROCESSA
↓
PROCESSA
↓
PROCESSA
↓
PROCESSA
↓
PROCESSA
E nunca para.
O que acontece?
A pilha cresce.
+----------------+
| PROCESSA |
+----------------+
| PROCESSA |
+----------------+
| PROCESSA |
+----------------+
| PROCESSA |
+----------------+
| PROCESSA |
+----------------+
| PROCESSA |
+----------------+
| PROCESSA |
+----------------+
| PROCESSA |
+----------------+
Cada chamada ocupa memória.
Mais cedo ou mais tarde...
Não haverá mais espaço.
Resultado:
Stack Overflow.
Em ambientes z/OS isso normalmente termina em um abend relacionado ao esgotamento da pilha ou da região disponível para a tarefa.
Por isso existe uma regra absoluta.
Toda recursão precisa possuir uma condição de parada.
Sem exceções.
O conceito mais importante para um Padawan
Existe um exercício mental muito útil.
Imagine cinco cópias do mesmo programa.
Não uma.
Cinco.
Cada uma com seus próprios parâmetros.
Cada uma em um ponto diferente da execução.
É exatamente isso que a recursividade produz.
Ela não "volta para o começo".
Ela cria outra execução.
Depois outra.
Depois outra.
Essa mudança de mentalidade faz toda diferença.
RECURSIVE x NON-RECURSIVE
O Enterprise COBOL precisa saber antecipadamente se um programa poderá chamar a si mesmo.
Por quê?
Porque isso muda completamente a estratégia de gerenciamento de memória.
Um programa tradicional pressupõe que existe apenas uma instância ativa.
Já um programa recursivo pode possuir dezenas ou centenas de ativações simultâneas.
É como comparar um apartamento ocupado por uma família com um hotel onde vários hóspedes utilizam quartos iguais ao mesmo tempo.
A estrutura física é parecida.
A forma de administrar os recursos é completamente diferente.
O papel do compilador
Quando um programa é preparado para suportar recursividade, o compilador gera código considerando que cada ativação precisará preservar seu próprio contexto.
Isso inclui:
parâmetros recebidos;
endereço de retorno;
variáveis locais;
registradores utilizados;
informações necessárias para retomar a execução exatamente do ponto em que foi interrompida.
Essa organização é uma das responsabilidades compartilhadas entre o Enterprise COBOL e o Language Environment (LE).
O maior erro de iniciantes
Existe um erro extremamente comum.
O desenvolvedor escreve:
WORKING-STORAGE SECTION.
01 WS-CONTADOR PIC 9(4).
Depois cria um algoritmo recursivo.
Na primeira chamada:
WS-CONTADOR = 1
Na segunda:
WS-CONTADOR = 2
Na terceira:
WS-CONTADOR = 3
Quando retorna...
O conteúdo foi alterado por outra ativação.
A lógica começa a produzir resultados inesperados.
O motivo?
Todas as ativações estão compartilhando a mesma Working-Storage.
Esse é um dos primeiros conceitos que todo Padawan precisa dominar.
Working-Storage: a memória compartilhada
A Working-Storage Section existe durante praticamente toda a vida do programa.
Ela é excelente para:
constantes;
tabelas fixas;
áreas de trabalho;
buffers reutilizados;
indicadores globais;
variáveis de controle que realmente precisam ser compartilhadas.
Mas ela não foi pensada para armazenar o estado individual de cada chamada recursiva.
Imagine uma lousa em uma sala de reunião.
Todos escrevem na mesma superfície.
O último a apagar ou sobrescrever vence.
É exatamente esse comportamento que pode ocorrer quando um programa recursivo utiliza Working-Storage para guardar informações específicas de cada ativação.
Local-Storage: a grande aliada da recursão
Foi justamente para resolver esse problema que surgiu a Local-Storage Section.
Ao contrário da Working-Storage, ela é criada novamente para cada ativação do programa.
Voltando ao exemplo da pilha:
+----------------------+
| PROCESSA(1) |
| LOCAL-STORAGE |
+----------------------+
| PROCESSA(2) |
| LOCAL-STORAGE |
+----------------------+
| PROCESSA(3) |
| LOCAL-STORAGE |
+----------------------+
Cada chamada possui sua própria cópia das variáveis locais.
Uma alteração realizada em uma ativação não interfere nas demais.
Esse comportamento torna a Local-Storage a escolha natural para programas recursivos e também para rotinas reentrantes e altamente concorrentes.
Uma analogia para nunca mais esquecer
Imagine uma cozinha industrial.
A Working-Storage seria uma única bancada compartilhada por todos os cozinheiros. Se um chef espalha farinha, outro pode encontrar essa farinha antes de começar sua própria receita.
A Local-Storage, por outro lado, funciona como uma bancada individual entregue a cada cozinheiro no início do trabalho. Cada um organiza seus ingredientes, prepara sua receita e, ao terminar, aquela bancada é desmontada sem afetar os demais.
Essa simples analogia ajuda a entender por que tantos problemas em programas recursivos desaparecem quando o estado da execução deixa de ser compartilhado e passa a ser local.
Conclusão da Parte 1
Ao ouvir a palavra recursividade, muitos programadores pensam imediatamente em exercícios acadêmicos como fatorial ou sequência de Fibonacci. No entanto, para quem trabalha com IBM Z, esse é apenas um detalhe.
O verdadeiro valor da recursão está em revelar como o Enterprise COBOL administra memória, organiza a pilha de execução, preserva contextos de chamadas e separa dados compartilhados de dados locais.
Mais do que aprender um algoritmo, estudar programas recursivos significa compreender os bastidores da linguagem — uma compreensão que será útil mesmo em projetos onde nenhuma rotina recursiva seja escrita.
No próximo café, vamos construir um programa recursivo completo em Enterprise COBOL, acompanhar cada chamada passo a passo dentro da pilha de execução, comparar sua execução com uma solução iterativa e descobrir em quais situações a recursão realmente se torna a melhor ferramenta para um desenvolvedor Mainframe.
Na Parte 2, aprofundaremos com código COBOL completo e comentado, execução passo a passo da pilha (stack), exemplos de fatorial, Fibonacci, árvores, XML/JSON, busca em diretórios, algoritmos clássicos (DFS, QuickSort e MergeSort) e diagramas de memória mostrando exatamente o que acontece a cada chamada recursiva.
Sem comentários:
Enviar um comentário