Introdução
A ideia de armazenar programas na memória junto com os dados que eles manipulam é amplamente atribuída a Von Neumann, cuja arquitetura se tornou a base dos computadores modernos. No entanto, conforme os programas cresceram em complexidade, tornou-se essencial desenvolver novas formas de organização do código, dando origem a diferentes modelos de programação.
Essa arquitetura inicial possuía uma estrutura mínima, composta por registradores — pequenas áreas de armazenamento dentro da unidade de processamento que guardam temporariamente dados e instruções em alta velocidade — e unidades de memória, responsáveis por armazenar um volume maior de informações temporárias, mas com acesso relativamente mais lento. Essa estrutura era suficiente para programas pequenos, mas, sem uma organização adicional, qualquer sistema mais complexo tornava-se difícil de gerenciar. Isso levou ao desenvolvimento de novas tecnologias, práticas e linguagens de programação, permitindo que os programadores se concentrassem na resolução de problemas em vez de lidar com detalhes do hardware.
Modelos de Programação
Modelo Imperativo
À medida que os programas cresceram em tamanho e complexidade, foi se necessário desenvolver uma estrutura de organização para eles. Com o objetivo de facilitar com que programadores organizassem seus códigos, a partir dos anos 60, os programas permitiram com que os seus códigos fossem decompostos em pequenos programas, sub-programas mais simples, e em peças mais manuseáveis conhecidas como funções ou subrotinas.
Este processo de decomposição (quebra) dos programas era conhecido anteriormente como programação estrutural. Hoje em dia, entretanto, é conhecido como modelo de programação imperativo, ou também como Paradigma Imperativo de Programação.
Neste modelo, para se implementar uma função é necessário que haja uma organização nos dados do programa. Dessa forma, a memória é dividida em duas regiões: uma contém o programa e outra os dados. Esta área de dados é subdividida em global data área (área de dados globais), run-time stack (pilha de execução) e em heap (área de alocação dinâmica de memória).
Quando um programa é executado, ele se utiliza do chamado Ponteiro da Pilha, para apontar para o valor que se encontra no topo da pilha de execução (run-time stack). Esta pilha contém um registro de chamada, ou registro de ativação, para cada função ou processo que foi invocado e que não está finalizado no programa.
A área de dados globais se refere aos dados e funções que são acessíveis globalmente pelo programa. Apesar do local onde estes dados globais são armazenados depender da implementação do compilador ou interpretador, é nesta área em que constantes, variáveis globais e possivelmente funções built-in/nativas são armazenadas.
O heap, ou área de alocação dinâmica de memória, diz respeito ao local em que armazena dados que são criados enquanto o programa está rodando, em execução. Estes dados não possuem nomes associados a eles, ao invés disso, as variáveis nominais (variáveis que têm nomes explícitos no código) que servem como ponteiros ou referências são responsáveis por indicar seu local na memória.
Ao separar por dados que “não possuem nomes associados a eles” e dados com nome próprio, deve-se entender que há situações em que dados possuem nomes explícitos no código e podem ser acessados diretamente por esse nome, por exemplo:
Estes dados, possuem seu valor armazenado diretamente na pilha de execução ou na área de dados globais. Por sua vez, os dados armazenados na heap são criados dinamicamente e não têm um nome fixo. Eles são acessados apenas por meio de ponteiros.
Relembrando ponteiros…
Variáveis do tipo ponteiro são responsáveis por indicar, armazenar endereços de memórias onde os valores estão guardados e não os valores em si. Dessa forma, é possível armazenar um endereço em um ponteiro mesmo que aquele endereço ainda não contenha um valor definido, assim, se está apenas reservando aquele espaço na memória para uso futuro.
O asterisco é responsável por indicar que a variável é um ponteiro, ou seja, ela armazenará um endereço de memória.
Porém, o asterisco também é utilizado para desreferenciar o ponteiro, ou seja, acessar ou modificar o valor armazenado naquele espaço da memória.
O objetivo principal do modelo imperativo consiste em: pegar dados via input, transformar eles via atualização de memória e então produzir um output baseado nessas mudanças.
Essa transformação de dados via atualização de memória consiste em alterar o estado do programa, que fica armazenado na memória, por meio de sequência de comandos, o que por sua vez produz um output. Esses comandos podem ser atribuições, loops ou chamadas de funções que possuem efeitos colaterais, ou seja, que alteram variáveis globais ou possuem parâmetros mutáveis.
Devido a isso, entende-se que o modelo imperativo foca no “como”, pois o programador precisa descrever, codificar exatamente o passo-a-passo de como uma tarefa deve ser realizada.
A programação orientada a objetos é vista como uma extensão deste modelo, visto que se utiliza do modelo imperativo porém com a adição de meios para descrever e criar classes de objetos, o que fornece uma nova forma de organização de código ao possibilitar criar funções (métodos) que são relacionadas a um tipo de objeto em específico. Isso fornece uma maior modularidade, reusabilidade e extensibilidade.
Modelo Funcional
No modelo funcional, o foco principal está nas chamadas de funções, em que, por meio destas e pela passagem de parâmetros, há a principal forma de se obter novos dados e informações. Diga-se nova pois as informações não são modificadas no modelo funcional, e sim novos valores são criados a partir de valores antigos.
Um modelo funcional puro, não permite que os valores existentes sejam modificados, em vez disso, novas “versões” dos dados são criados. Apesar disso, a maioria das linguagens funcionais permitem atualizações de memória devido a influência do do modelo imperativo.
Em questão de arquitetura, a diferença entre o programa e o dado é eliminado de forma que uma função é entendida como um dado, assim como qualquer outro elemento, por exemplo um inteiro.
A área de dados globais ainda se faz presente, porém possui uma participação menor. Entretanto, a pilha de execução (run-time stack) se torna mais importante, visto que a maior parte do trabalho é realizada por meio da chamada de funções. Apesar deste modelo possuir como característica criar novos dados ao invés de modificar antigos, os dados são alocados dinamicamente, porém, uma vez que criados no heap, eles não são modificados, pelo menos não em um modelo funcional puro e isso, por sua vez, pode resultar em possíveis problemas de performance. Esse fator explica a influência das linguagens imperativas: solucionar os possíveis problemas de performance que venham a surgir.
Modelo Lógico
No modelo lógico, o programador não escreve nenhum programa. Na verdade, é preciso fornecer um conjunto de fatos e regras em que, deste conjunto, um único programa tenta responder questionamentos por meio de sim ou não. Essa implementação ocorre por meio de máquinas virtuais. Especialmente em Prolog, a principal linguagem deste modelo, ainda há o conceito de heap, pois é possível definir novas regras ou retirar a medida que o programa é executado.
Conclusão
Ao longo da história da computação, diferentes modelos de programação surgiram para solucionar desafios específicos e tornar o desenvolvimento de software mais eficiente. O modelo imperativo reflete a forma tradicional de estruturar programas, focando no controle explícito do estado. O modelo funcional, por sua vez, busca minimizar efeitos colaterais e enfatiza a imutabilidade. Já o modelo lógico propõe uma abordagem declarativa, em que o foco está na formulação de regras e fatos para resolver problemas.
Embora esses modelos sejam distintos, a prática moderna da programação muitas vezes combina elementos de diferentes paradigmas. Linguagens como JavaScript, Python e Scala, por exemplo, incorporam tanto princípios imperativos quanto funcionais. Compreender essas abordagens permite que programadores escolham as melhores estratégias para cada problema, tornando-se mais versáteis e eficientes no desenvolvimento de software.
Referências:
Lee, K. D. (2007). Foundations of programming languages. Springer.
Ranta, A. (2012). Implementing programming languages. College Publications.
Top comments (0)