Prévia do material em texto
e-Book 2 MSC Rafael De Moura Moreira PARADIGMAS DE LINGUAGENS DE PROGRAMAÇÃO Sumário INTRODUÇÃO ������������������������������������������������� 3 PARADIGMAS DE PROGRAMAÇÃO ��������������� 4 PROGRAMAÇÃO IMPERATIVA ���������������������� 6 PROGRAMAÇÃO ORIENTADA A OBJETO ���� 11 PROGRAMAÇÃO FUNCIONAL����������������������19 PROGRAMAÇÃO LÓGICA �����������������������������26 PROGRAMAÇÃO ORIENTADA A EVENTOS �� 28 PROGRAMAÇÃO CONCORRENTE ����������������31 CONSIDERAÇÕES FINAIS ����������������������������38 REFERÊNCIAS BIBLIOGRÁFICAS & CONSULTADAS ��������������������������������������������42 3 INTRODUÇÃO Olá, estudante! Neste material iremos conhecer um pouco sobre alguns paradigmas de programação – maneiras diferentes de pensar e organizar um programa. Conheceremos os conceitos que definem os pa- radigmas mais conhecidos e algumas linguagens de programação que os representam bem. Além disso, estudaremos também alguns conceitos importantes que nos ajudarão a compreender os diferentes paradigmas, como subprogramas, clas- ses, objetos, eventos e conceitos de programação concorrente. Vamos lá? Bons estudos! 44 PARADIGMAS DE PROGRAMAÇÃO Paradigmas de programação são formas diferentes de se pensar um programa. Diferentes paradigmas possuem diferentes objetivos, e podem alterar ra- dicalmente a forma de se programar. Eles podem focar em formas diferentes de se modelar dados ou de passar instruções para o computador. Para adotarmos um paradigma específico, é ne- cessário que a linguagem ofereça os recursos apropriados para que os conceitos do paradigma escolhido possam ser devidamente implementados. Na prática, várias linguagens surgiram com foco na adoção ou suporte de um paradigma especí- fico, e por conta disso é comum que as próprias linguagens sejam classificadas em função de seu paradigma. Porém, frequentemente é possível utilizar uma mesma linguagem para programar em mais de um paradigma diferente. Da mesma maneira, é comum que conceitos relacionados a mais de um paradigma coexistam em um mesmo programa. Existe uma enorme diversidade de paradigmas, e estudiosos chegam a debater se certas caracte- rísticas de algumas linguagens constituem para- digmas próprios ou se elas seriam apenas casos 55 particulares de outros paradigmas. Estudaremos aqui os seguintes paradigmas: y Programação imperativa; y Programação orientada a objetos; y Programação funcional; y Programação lógica; y Programação orientada a eventos; y Programação concorrente. Diferentes estudiosos podem divergir um pouco na forma de classificar estes paradigmas. É comum, por exemplo, considerar que a programação orien- tada a objetos seja um caso bastante particular de programação imperativa – Sebesta (2018), por exemplo, considera que a orientação a objeto é apenas um conjunto de recursos adicionais que algumas linguagens imperativas passaram a oferecer. Não nos aprofundaremos aqui em discussões acadêmicas e seguiremos a interpretação mais comum. Mas é importante saber que existem outros paradigmas e outras abordagens para estudá-los. 6 PROGRAMAÇÃO IMPERATIVA O paradigma imperativo é o mais natural do ponto de vista do hardware dos sistemas computacionais modernos. Seu nome deriva do modo imperativo, a forma verbal que caracteriza ordens ou instru- ções. Na programação imperativa, o programa é constituído por uma sequência de comandos que o computador deverá executar. É intuitivo imaginar esse modo na prática. O próprio hardware foi projetado de maneira imperativa: a linguagem binária nativa das máquinas consiste em um conjunto de operações elementares, e os programas são sequências específicas dessas instruções. Chega a ser difícil imaginar um paradigma que não seja imperativo. Mas é comum que sejam feitas comparações entre o paradigma imperativo (e seus paradigmas derivativos) com os paradigmas considerados declarativos, como o funcional e o lógico (que serão explicados logo abaixo), em que o programador foca em explicar o que o programa deve fazer, e não como fazer. Não é incomum encontrarmos os nomes “impe- rativo” e “procedural” sendo usados de maneira intercambiável, mas podemos considerar o pa- 7 radigma procedural como um “caso especial” da programação imperativa. Programação procedural A programação procedural é uma forma de pro- gramação imperativa: o programador organiza sequências de comandos ou instruções que o computador deverá executar. O que caracteriza a programação procedural é a forma de organizar as instruções: o programa não será formado por um único bloco longo e contínuo de comandos. Ao invés disso, as instruções podem ser agrupadas em módulos, ou subprogramas. Um programa passa a ser uma coleção de subprogra- mas, cujo cada subprograma é uma sequência de comandos seguindo o paradigma imperativo. Subprograma Subprogramas podem ter diferentes nomes em diferentes linguagens, e em alguns casos, os nomes diferentes podem representar diferenças sutis entre elas. Esses nomes incluem sub-rotinas, procedimen- tos e funções. Em algumas linguagens, a palavra “procedimento” diz respeito a um subprograma qualquer, uma sequência de instruções. Já uma função seria um “caso especial” de procedimento que retorna um valor. Outras linguagens não fazem essa diferenciação. Neste material, deste ponto 8 em diante, utilizaremos “função” como sinônimo de subprograma. Linguagens de programação costumam oferecer uma sintaxe própria ou mesmo uma palavra-chave específica para definir uma função. Normalmente, junto dessa definição, devemos dar um nome para a função, informar uma lista de parâmetros, e dependendo da linguagem, o tipo de seu retorno. Quando o nome de uma função “A” qualquer aparece durante a execução de outra função “B”, dizemos que a função “A” está sendo chamada. Quando uma função é chamada, a execução da função que a chamou é interrompida momentaneamente e o fluxo de execução passa para a função chamada. Esta será executada até o final, e em seguida a execução da função anterior é retomada do ponto que parou. Dizemos que funções respeitam o escopo dos dados do programa. Isso significa que dados inter- nos de uma função não podem ser acessados ou modificados por outras funções, garantindo uma melhor modularização do programa. Como os dados de uma função são privados, para que dados sejam passados de uma função para a outra, existe o conceito de parâmetros de uma função. Parâmetros são “dados de entrada” para uma função. Quando a função é chamada, quem 9 a chamou pode passar um conjunto de dados para ela. Essa passagem pode ser uma mera cópia dos valores – a chamada passagem por valor, ou passagem por cópia, na qual a função que foi chamada receberá apenas uma cópia dos dados e não poderá modificar os dados originais – ou uma referência para a região de memória contendo os dados originais – conhecida como passagem por referência, que permite que a função altere os dados originais. Após o final de sua execução, uma função também pode fornecer dados para a função que a chamou. Chamamos esses dados de saída de uma função de retorno da função. O retorno é comum em fun- ções que possuem uma resposta – por exemplo, uma função que resolva um problema matemático, como o cálculo de uma raiz quadrada – ou em funções que devem informar um status, como se suas instruções foram executadas com sucesso ou se ocorreu algum erro. Como o programa escrito em uma linguagem pro- cedural é constituído por uma coleção de funções, é importante que o computador saiba qual delas é a primeira função que ele deverá executar. É comum que as diferentes linguagens tenham o conceito de uma função principal, frequentemente chamada 10 de main. Ela é considerada o ponto de entrada do programa. Ela será responsável por inicializações em geral e por fazer as primeiras chamadas para outras funções. Após todas as execuções das ou- tras funções, o fluxo sempre retornarápara essa função principal antes de encerrar o programa. Diversas linguagens de programação estruturadas surgidas entre o final dos anos 50 e os anos 70 são linguagens procedurais, como ALGOL, COBOL, BASIC, FORTRAN e C. Várias outras linguagens que suportam outros paradigmas, como o orientado a objeto, também suportam a programação proce- dural, como o C++, o C# e o Python. 1111 PROGRAMAÇÃO ORIENTADA A OBJETO Em muitas literaturas, a programação orientada a objeto é considerada um caso particular ou uma extensão da programação imperativa. Isso ocorre porque ela também possui estratégias de modula- rização do código onde, internamente, as unidades serão sequências de comandos ou instruções para o computador. Porém, a modularização adotada pela progra- mação orientada a objeto é bastante diferente da programação procedural e envolve diversos conceitos novos. Esse paradigma pode ser utilizado para modelar no programa entidades do mundo real. As entida- des serão representadas por objetos. Os objetos possuem suas próprias informações internas, bem como suas próprias ações e habilidades, e o programa resulta da comunicação entre diferentes objetos. Vamos observar alguns conceitos básicos. Classes e objetos Cada entidade envolvida no modelo será represen- tada por um objeto. Um objeto possui diversas informações particulares, para uso interno. Cada uma das informações internas de um objeto é cha- 1212 mada de atributo. O conjunto de informações de um objeto em um dado momento é o seu estado. Além de informações, objetos também possuem suas próprias ações, que são representadas por métodos. Um método é uma forma de subprograma, e possui muitas características em comum com as funções da maioria das linguagens procedurais. Dentro dos métodos, trabalhamos de maneira imperativa, listando instruções que deverão ser seguidas pelo computador. A grande diferença entre uma função convencional e um método é conceitual: a função tipicamente é chamada por outra função e pode receber dados como parâmetros para utilizar em sua computação. O método é chamado pelo objeto, e apesar de poder receber parâmetros, consideramos que ela age primariamente sobre o próprio objeto, acessando seus atributos e podendo alterar seu estado. É possível que vários objetos possuam caracte- rísticas em comum. Por exemplo, ao projetarmos um sistema acadêmico onde cada aluno possa consultar suas disciplinas, trabalhos pendentes, notas e faltas, cada aluno seria um objeto diferente. Porém, sabemos que eles terão características em comum: todo aluno terá um login, uma senha, um nome, um documento de identidade, um número de matrícula, uma lista de disciplinas etc. 1313 Podemos criar uma classe para representar alu- nos. A classe será a ideia abstrata de aluno, e diz quais informações (atributos) e comportamentos (métodos) cada objeto aluno possuirá. Um objeto é uma instância de uma classe e representa uma entidade real, concreta. Princípios da programação orientada a objeto Quando bem utilizada, a programação orientada a objeto oferece uma modularização bastante pode- rosa. Classes podem ser desenvolvidas de maneira independente por diferentes programadores e con- seguirão interagir no programa sem dificuldades. Além disso, é fácil expandir as funcionalidades de uma classe ou corrigir erros com impacto mínimo no restante do programa. Para que todos esses benefícios possam ser des- frutados adequadamente, é importante que alguns princípios básicos sejam respeitados: encapsula- mento, abstração, herança e polimorfismo. O princípio do encapsulamento dita que cada ob- jeto deve ser responsável por seu próprio estado. Isso significa que os atributos de um certo objeto não devem sofrer interferência de outros objetos. Idealmente, apenas o próprio objeto pode alterar seus atributos. Para que isso seja atingido, atributos 1414 devem ser privados, podendo ser alterados apenas dentro dos métodos daquele objeto. O próximo princípio, abstração, tem bastante relação com o princípio do encapsulamento. Ele dita que um objeto deve “esconder” toda sua complexidade e fornecer uma interface simples para interagir com o restante do programa. Isso significa que quando planejamos uma classe, ela deve ser a mais autossuficiente possível, e os métodos daquela classe devem ser capazes de resolver todos os problemas relacionados a obje- tos daquela classe. Esses métodos, por sua vez, devem ser fáceis e intuitivos de serem chamados por outros objetos. A comunicação entre objetos (seja de uma mesma classe ou não) ocorre através dos métodos: um método de um objeto recebe outro objeto e chama os métodos desse objeto passando diferentes parâmetros. O mais intuitivo dos princípios provavelmente é a herança. Ela é bastante similar à herança que co- nhecemos na biologia. Quando dizemos que uma classe é herdeira de outra classe, a “subclasse” automaticamente possui os mesmos atributos e métodos da “superclasse”, sem que precisemos explicitamente reimplementar todo o código correspondente. Esse princípio é extremamente poderoso, permitindo uma modularização muito grande e evitando a repetição desnecessária de 1515 código, facilitando e agilizando não apenas o de- senvolvimento, mas também testes, atualizações e correções. O último princípio é o polimorfismo. Essa palavra vem da combinação das palavras de origem grega poli e morphos, que significam, respectivamente, “várias” e “formas”. O princípio do polimorfismo dita que um mesmo objeto pode ser interpretado como pertencente a diferentes classes. O polimorfismo pode se manifestar de diferentes maneiras, dependendo dos recursos disponíveis na linguagem. A forma mais básica está relacionada à herança: se uma classe “Aluno” é herdeira de uma classe “Pessoa”, objetos da classe Aluno podem ser reconhecidos também como objetos da classe “Pessoa”, e funções projetadas para trabalhar com Pessoa também conseguem lidar com Aluno sem a necessidade de ajustes. Podemos observar o polimorfismo se manifestando de outras formas. Na linguagem Java, por exemplo, existe um recurso chamado de “interface”. Uma interface não é uma classe, mas uma espécie de “contrato”. A interface prevê alguns métodos que deverão ser implementados pelas classes que aderirem a esse “contrato”. Objetos de qualquer classe que implemente a interface podem ser tra- tados simplesmente como objetos dessa interface. 1616 Outro exemplo mais extremo é o chamado “duck typing” do Python. Esse princípio é representado por um lema em inglês, que traduzido para o português fica “Se ele faz ‘quá-quá’ como um pato e anda como um pato, então ele é um pato”. Isso significa que a maioria das funções e classes em Python não se importa com qual a classe original de um certo objeto. Se ele possui os métodos esperados, sem problemas. Isso permite a elaboração de códigos extremamente genéricos e reutilizáveis. Linguagens orientadas a objeto Após sua explosão de popularidade ainda nos anos 80, as linguagens orientadas a objeto se tornaram o padrão de fato para projetos grandes e comple- xos, e até hoje elas são extremamente populares. Na prática, muitas linguagens orientadas a objeto são consideradas multiparadigma, podendo ser utilizadas também de maneira imperativa pura, de maneira procedural ou combinando paradigmas em um mesmo programa, por exemplo, ao utilizar classes e objetos simultaneamente com concei- tos de outros paradigmas, como o tratamento de eventos ou funções lambda. Após o surgimento das primeiras linguagens orien- tadas a objeto, como Simula e Smalltalk nos anos 70, surgiram evoluções de outras linguagens já existentes incorporando o suporte à programação 1717 orientada a objeto, que é o caso do C++ (baseado no C), Delphi (baseado no Pascal) e VisualBasic (baseado no BASIC). Algumas dessas linguagens seguem evoluindo e apresentando novos recursos até hoje, como o C++ e o VisualBasic (atualmente parteda família .NET da Microsoft). Outras linguagens também surgiram, aperfeiçoando ideias trazidas por essas primeiras linguagens ou focando em diferentes aspectos da programação. Um exemplo bastante clássico é o Java, que utiliza a ideia de uma máquina virtual permitindo que um mesmo programa possa ser executado em diversas arquiteturas diferentes de computador, e popularizou diversos conceitos que acabaram sendo incorporados por outras linguagens poste- riores, como as interfaces. A Microsoft oferece hoje a família .NET, que possui linguagens de diferentes paradigmas, incluindo uma linguagem funcional, F#. Mas suas linguagens mais famosas são o C# (uma linguagem originalmente baseada no Java) e o VB.NET (uma linguagem baseada no VisualBasic). Dentre as vantagens estão a possibilidade de interação entre códigos escritos nas diferentes linguagens da família. Um programa em C# pode importar classes escritas em VB.NET, por exemplo. 1818 Várias linguagens de scripting (programas curtos para automatizar pequenas tarefas) populares também são orientadas a objeto, como o JavaScript (muito utilizado no desenvolvimento de páginas web) e o Python (popular na automação de tarefas e processamento de dados). 19 PROGRAMAÇÃO FUNCIONAL Enquanto a programação orientada a objeto ainda se aproxima bastante da programação imperativa – afinal, apesar de adotarmos regras e estruturas específicas para auxiliar na modularização, nossos programas ainda armazenam dados em variáveis e seguem instruções sequenciais – outros pa- radigmas podem ser radicalmente diferentes. O paradigma funcional é um deles. Um programa escrito utilizando uma linguagem funcional é, em tese, mais legível, mais seguro e menos propenso a erros. A programação funcio- nal é considerada mais determinística do que a programação imperativa. Dizemos que um programa ou algoritmo é consi- derado determinístico se, dadas as mesmas entra- das, sempre produz as mesmas saídas. Por mais estranho que isso possa soar, programas escritos de maneira puramente imperativa ou orientada a objeto podem se comportar de maneira aparente- mente não determinística. Isso ocorre porque os programas (assim como os objetos) possuem um estado, que é dado pelos valores de todas as variáveis. Em projetos muito grandes, uma função pode ter diversas variáveis 20 internas, além de interagir com variáveis externas através de parâmetros, referências, ou até mesmo variáveis globais – variáveis criadas fora de qualquer escopo e, portanto, passíveis de serem alteradas por qualquer função. Dessa maneira, uma função recebendo um mesmo parâmetro múltiplas vezes pode retornar valores diferentes, caso ela dependa de certas variáveis espalhadas pelo código. Essas variáveis acabam se comportando como entradas “ocultas” ou “im- plícitas” das funções e fazem com que seja muito mais difícil analisar seu comportamento ou prever seu resultado final. Um programa puramente funcional não possui estado. Ele não armazena dados em variáveis. Ele possui apenas funções, e essas funções tentam ao máximo seguir o comportamento de uma função matemática. Isso torna sua análise bastante simples – afinal, funções matemáticas costumam ser expressões simples, sem estruturas como malhas de repetição. Além disso, devido à ausência de um estado que pode se alterar, elas são completamente deter- minísticas e sempre produzirão a mesma saída para uma certa entrada. Você provavelmente já estudou funções matemáticas no passado, mas vamos revisar brevemente alguns 21 conceitos e partir deles para ver outros conceitos importantes para a programação funcional, como as funções lambda. Funções matemáticas Uma função matemática é uma regra para mape- armos valores entre dois conjuntos: o conjunto domínio e o conjunto imagem. Cada valor do domínio pode ser mapeado para exatamente um valor da imagem. Um valor do domínio jamais poderá ser mapeado para múltiplos valores dife- rentes da imagem. Vamos tomar como exemplo a seguinte função: f(x)=2x Se o conjunto domínio for (1, 2, 3, 4), ao aplicarmos a regra, obteremos os valores (2, 4, 6, 8). Ou seja, a função f(x) mapeia os valores 1, 2, 3 e 4 para os valores 2, 4, 6 e 8, respectivamente. Funções matemáticas podem ser constituídas por operações matemáticas, por outras funções, por chamadas para a própria função (chamamos de “chamadas recursivas”) ou até mesmo por expressões condicionais. Mas não temos outras estruturas comuns na programação imperativa, como as malhas de repetição. Um exemplo puramente matemático que envolve expressões condicionais e chamadas recursivas 22 é a sequência de Fibonacci. Seus dois primeiros termos são iguais a 1. Qualquer outro termo é definido pela soma de seus dois antecessores ime- diatos. Sendo assim, podemos definir a sequência de Fibonacci com a seguinte função: 𝐹𝐹 𝑛𝑛 = % 1, 𝑠𝑠𝑠𝑠 𝑛𝑛 = 0 𝑜𝑜𝑜𝑜 𝑛𝑛 = 1𝐹𝐹 𝑛𝑛 −1 +𝐹𝐹 𝑛𝑛 −2 , 𝑠𝑠𝑠𝑠 𝑛𝑛> 1 Note que é uma regra rigorosa muito bem defini- da, fácil de ler e completamente determinística, com qualquer valor de entrada gerando sempre o mesmo valor de saída. Funções compostas Outra possibilidade que a matemática nos oferece é a composição de funções. Considere as funções abaixo: f(x)=2x g(x)=3x+2 Podemos definir uma função h(x) dada pela com- posição de ambas as funções. A notação para a composição de funções é: h=gof(x) Essa notação significa: h(x)=f(g(x)) Na prática, isso significa que o resultado de h(x) será determinado quando substituirmos “x” em 23 f(x) por g(x) – ou seja, aplicar uma função como entrada ou parâmetro para outra função. h(x)=2(3x+2) h(x)=6x+4 Ao contrário das linguagens imperativas tradicio- nais, funções podem receber outras funções (e não apenas valores, como variáveis, constantes ou o retorno de uma função) como parâmetro. Funções lambda Quando estudamos funções ao falar sobre progra- mação procedural, definimos funções como sendo subprogramas que possuem um nome, uma lista de parâmetros e, opcionalmente, um retorno. A programação funcional nos permite ter funções sem nome. Chamamos essas funções anônimas de funções lambda. Isso abre possibilidades impensáveis nas linguagens imperativas tradicionais. Uma função pode criar novas funções. Podemos, inclusive, ter funções que retornam funções. Como não precisamos usar um comando para de- finir previamente e dar um nome a uma função em um bloco de comandos avulso, é possível no meio de uma função surgir a definição de um lambda em função de diferentes condições e resultados 24 de outras funções, permitindo a “geração” de uma função nova dinamicamente. Linguagens funcionais A primeira linguagem de programação funcional foi a LISP. Ela foi proposta por John McCarthy, do MIT, ainda no final da década de 50 como uma ten- tativa de descrever matematicamente programas de computador. Um de seus conceitos-chave era a função EVAL, que serviria para avaliar qualquer outra expressão. Dois pesquisadores, Stephen B. Russell e Daniel J. Edwards concluíram que uma implementação real da função EVAL seria, na prática, um interpretador de LISP – ou seja, um programa de computador que executa programas escritos em LISP. Eles conseguiram realizar essa implementação com sucesso. O interpretador LISP, apesar de nos círculos de programadores ter a reputação de ter uma sinta- xe supostamente complicada por conta do uso frequente de parênteses aninhados, possui uma sintaxe tão simples que é possível escrever de maneira compacta uma implementação da função EVAL em LISP, o que na prática significa que é fácil escrever um interpretador LISP em LISP. 25 Apesar de a programação funcional existir desde a época das primeiras linguagens procedurais e ter tantos benefícios, ela nunca se tornou o paradig- ma dominante no mercado. Notamos até hoje um domínio muito grande de paradigmas relacionados ao imperativo, principalmente a programação orien- tada a objeto, emboraatualmente a procura por desenvolvedores familiares com esse paradigma esteja em alta por conta da adoção de linguagens funcionais em certos sistemas que devem ser extremamente determinísticos e rigorosos, como sistemas bancários. As linguagens funcionais mais utilizadas atualmente não são puramente funcionais. É comum que elas incorporem alguns elementos imperativos, mas utilizem predominantemente conceitos típicos da programação imperativa. Várias linguagens funcionais são consideradas dialetos de LISP, como Common Lisp, Clojure e Scheme, enquanto outras foram fortemente influenciadas direta ou indiretamente por ela, como Elixir, Haskell e F#. Além disso, várias linguagens utilizadas predomi- nantemente para o desenvolvimento em outros paradigmas, como o orientado a objeto, incorpo- raram alguns conceitos de programação funcional, como o C++, Java, JavaScript, Go, Rust e Python. O Python, por exemplo, suporta funções lambda. 2626 PROGRAMAÇÃO LÓGICA O nome “programação lógica” pode soar confu- so em um primeiro contato: todas as formas de programação usam lógica, correto? Então o que diferencia esse paradigma dos outros? O paradigma lógico é fortemente baseado na ló- gica formal – um campo da matemática que lida com provas formais, argumentação e teoria de conjuntos, entre outras coisas. Esse paradigma é declarativo, significando que seus programas não são formados por instruções, e sim por um conjunto de afirmações. Um programa será constituído por uma série de regras ou afirmações, chamados de predicados. Essas regras podem relacionar diferentes dados. O programa será executado por uma espécie de loop infinito, onde o usuário pode fazer consultas. O resultado dessas consultas é exibido imediata- mente na tela e em seguida o programa torna a aguardar novas consultas. Uma consulta pode ser uma afirmação lógica. O programa testará todas as combinações possíveis de dados e relações declarados em seus predica- dos para determinar se a consulta é verdadeira ou falsa, ou para quais valores ela é verdadeira, e irá fornecer essa resposta. 2727 Temos poucos exemplos de linguagens de progra- mação lógica, sendo a mais conhecida o PROLOG e seus dialetos. Essa linguagem teve popularidade limitada em meios acadêmicos, principalmente dentre pesquisadores interessados na automação de produção de provas formais, simplificação de predicados lógicos e também em formas diferentes de representar conhecimento em alguns modelos de inteligência artificial. 28 PROGRAMAÇÃO ORIENTADA A EVENTOS A programação orientada a eventos é uma for- ma de planejar um programa que precise reagir a diferentes ocorrências com frequência. Essas ocorrências podem ser as mais diversas: uma interação do usuário, um sinal captado por um sensor ou mesmo uma comunicação de outro processo ou thread. Um caso fácil de visualizar esse paradigma é em programas que possuem uma interface gráfica. Considere, por exemplo, um editor de texto. Quan- do abrimos um editor de texto, ele não faz muita coisa. É normal que ele apenas mostre uma tela em branco (ou um texto parcialmente preenchido, quando abrimos um arquivo pronto) e um cursor piscando em uma certa posição. Ele pode passar horas a fio nesse estado, sem realizar nenhuma ação interessante. Quando pressionamos uma tecla, porém, ele colo- ca a letra correspondente a essa tecla na posição onde o cursor estava piscando. Também podemos clicar em botões: se você clica no botão de salvar, por exemplo, o programa mostra um pop-up para você escolher a pasta e dar um nome ao arquivo. 29 Um evento é uma notificação de que algo ocorreu. Essa notificação pode partir do sistema operacio- nal, do hardware ou de outros programas que se comunicam com o nosso. O programa orientado a eventos tipicamente possui um loop principal que mantém o programa aberto (ou um sistema onde o programa pode entrar em espera e ser notificado e reativado quando um evento ocorre) e uma série de tratamentos para eventos específicos. Os tratamentos são funções que serão executadas sempre que um evento es- pecífico ocorrer. Em um programa com interface gráfica – como o editor de texto que utilizamos como exemplo – existem diferentes elementos gráficos (frequente- mente objetos, em linguagens orientadas a objeto), conhecidos como widgets. Cada widget possui uma funcionalidade específica. Interações com esse widget geram eventos, e esses eventos provocam a execução do tratamento para esse evento, que normalmente é a funcionalidade do widget. A programação orientada a evento não costuma aparecer “sozinha” e é um recurso utilizado em conjunto com outros paradigmas, como proce- dural (através de funções que tratam eventos) ou orientado a objeto (em que até mesmo os eventos podem ser objetos). Ela pode ser implementada 30 em diversas linguagens diferentes e depende mais de recursos disponíveis (como um sistema de interrupção por hardware ou um sistema de notificação do sistema operacional) do que de recursos da linguagem. 3131 PROGRAMAÇÃO CONCORRENTE Um aspecto cada vez mais presente na computa- ção é a realização de diferentes tarefas ao mesmo tempo. O seu navegador de internet, por exemplo, precisa trocar mensagens com o servidor, executar scripts, realizar interação com o usuário etc. Em computadores com um único processador, essas diferentes tarefas precisariam necessariamente se revezar: uma delas ocupa o processador por uma pequena quantidade de tempo, e em seguida o libera para que outra tarefa o ocupe. Atualmente, vários computadores possuem múlti- plos processadores. Isso pode ocorrer em grandes sistemas presentes em empresas que utilizam ou vendem serviços de computação “pesada”, como Amazon e Google, ou mesmo em dispositivos de uso pessoal: desktops, laptops, e até mesmo vários modelos de smartphone comumente oferecem múltiplos processadores. Isso permite que diversas tarefas sejam realizadas de maneira paralela, ou seja, ao mesmo tempo. No exemplo do navegador, é possível, por exemplo, que um núcleo de processamento cuide da comunica- ção com o servidor, enquanto outro núcleo executa scripts, e assim sucessivamente. 3232 Projetar programas para execução paralela, ou concorrente, exige suporte por parte do sistema operacional, que fará boa parte das tarefas de ge- renciamento e oferecerá recursos para sincroniza- ção, controle, distribuição das tarefas e resolução de alguns problemas comuns que discutiremos em breve. Também é importante que a linguagem utilizada ofereça algum nível de suporte para a programa- ção multiprocessada. Linguagens de diferentes paradigmas oferecem esse recurso em diferentes níveis. Linguagens como C++ (orientada a objeto), C e FORTRAN (procedurais) possuem bibliotecas para permitir essa implementação. Outras, como F# (funcional), Java, C# e Python (orientadas a objeto) oferecem recursos de forma nativa. Utilizar esses recursos para criar programas que executam de maneira paralela oferece alguns benefícios, sendo o mais óbvio deles o desempe- nho: se diferentes partes de um programa forem executadas simultaneamente por processadores diferentes, o programa terminará de executar mais rápido do que se todas as partes fossem executa- das sequencialmente por um único processador. Imagine quatro pessoas empacotando compras de supermercado – cada um colocando diferentes mercadorias em diferentes sacolas – e compare 3333 com uma pessoa sozinha empacotando as mes- mas mercadorias. Porém, nem todo tipo de tarefa pode ser paralelizado, e por isso nem todos os programas conseguirão se beneficiar diretamente da programação con- corrente. Além disso, a programação concorrente também pode criar alguns problemas que não temos quando temos um único programa sendo executado de maneira linear. Vamos definir alguns conceitos básicos relacionados à programação concorrente, enunciar alguns dos problemas mais comuns e descrever as principais soluçõespara eles. Todos esses conceitos devem ser estudados de maneira mais aprofundada em disciplinas ligadas a sistemas operacionais ou à própria programação concorrente. Conceitos básicos Os sistemas operacionais costumam trabalhar com o conceito de processo. Um processo é uma abstração para um programa em execução. Ele possui um cabeçalho com informações de controle e uma região de código, que são as suas instruções. É possível, porém, que um processo possua diversos blocos de código para serem executa- dos paralelamente. Todos eles compartilham o mesmo cabeçalho, pois são o mesmo programa. 3434 Mas eles podem ser executados por núcleos de processamento diferentes. Chamamos esses có- digos paralelos de threads. E um programa que trabalha com múltiplos threads pode ser chamado de multithread. Quando possuímos mais threads do que núcleos disponíveis, é preciso que haja um mecanismo de controle para permitir que cada tarefa seja execu- tada sem que nenhuma delas monopolize o uso do processador. Os sistemas operacionais costumam vir com um escalonador de tarefas para realizar esse controle, que tipicamente permitem que uma tarefa seja executada por um instante de tempo, em seguida eles a pausam e permitem que outra tarefa também seja executada por um instante de tempo, e assim sucessivamente. Tempo não é o único critério: algumas tarefas podem pos- suir prioridade maior do que outras. Além disso, quando uma tarefa precisa aguardar algo (como uma operação de entrada e saída de dados ou o resultado de outra tarefa), ela é colocada em espera e o processador é liberado para outras tarefas. Problemas de concorrência Imagine duas tarefas distintas (por exemplo, duas funções sendo executadas em paralelo) e ambas possuem instruções para manipular um mesmo valor na memória. É possível que a ordem que duas 3535 operações são realizadas afete o resultado. Sendo assim, é difícil prever o resultado da operação se não temos controle sobre qual tarefa conseguirá ser executada primeiro. Chamamos esse problema de condição de corrida. Imagine agora outro caso. Imagine que existam dois recursos – dados na memória, dispositivos de entrada/saída ou outro motivo qualquer – e duas tarefas sendo executadas. Uma das tarefas possui, no momento, acesso a um dos recursos, e a outra tarefa possui acesso ao outro recurso. Imagine que a primeira tarefa precisa de ambos os recursos para sua execução, e irá liberar ambos apenas após o seu final. O que ocorre se a outra tarefa também precisa de ambos os recursos e só irá liberá-los após o final de sua execução? Ambas as tarefas ficarão eternamente cada uma bloqueando um dos recursos e esperando que a outra libere o outro recurso. Chamamos essa situação de deadlock. Gerenciando a sincronia Algumas soluções diferentes são adotadas para evitar problemas com a condição de corrida ou com o deadlock. Cada um deles será melhor em diferentes casos e é até possível utilizar alguma dessas soluções para implementar a outra (ex: uti- lizar um monitor para implementar um semáforo). 3636 A técnica mais simples é um semáforo. Semáforos podem ser contadores ou binários. Um semáforo do tipo contador serve quando temos diversos recursos disponíveis – por exemplo, regiões de memória para armazenar dados. Toda tarefa que for consumir esses recursos deve incrementar o contador do semáforo indicando a quantidade de recursos sendo utilizada. Ao finalizar sua execução e liberar os recursos, ela deve decrementar o con- tador de acordo. Os binários são mais simples, e possuem apenas dois valores possíveis: o recurso está ocupado ou livre. Antes de usar um recurso, a tarefa deve verificar se ele está livre. Caso esteja, ela o marca como ocupado e o usa. Ao final de sua execução, torna a sinalizá-lo como livre. A próxima técnica é o monitor. Um monitor é uma estrutura que contém um mutex (uma trava) para o recurso a ser utilizado e coleções de tarefas in- teressadas nos recursos, bem como informações pertinentes para gerenciar as tarefas. Se uma tarefa utilizando um recurso precisa aguardar alguma condição, o monitor irá liberar o recurso para outra tarefa. Por fim, pode ser necessário que duas tarefas concorrentes conversem entre si. Para isso existe a troca de mensagens, que pode ser síncrona ou assíncrona. Existem várias formas diferentes de implementar essas mensagens. Mas de maneira 3737 simplificada, na comunicação síncrona é neces- sário que ambas as tarefas estejam disponíveis para comunicação, de modo que uma tarefa não interrompa a execução da outra, enquanto na assín- crona uma tarefa pode transmitir uma mensagem e prosseguir com sua execução sem aguardar resposta, e a outra responderá quando possível. 38 CONSIDERAÇÕES FINAIS Um programa pode ser pensado de muitas maneiras diferentes. Uma sequência de instruções, apesar de ser a forma mais natural para as arquiteturas de hardware que utilizamos em nosso dia a dia, não é a única e nem necessariamente a melhor para resolver certos tipos de problemas. Diferentes paradigmas de programação, ou for- mas de estruturar e modelar nossos programas, serão mais adequados em diferentes situações ou contextos. Nossos programas podem ser imperativos, sendo constituídos apenas por diversas instruções se- quenciais. Essas instruções podem ser agrupadas de maneiras diferentes, como em subprogramas (ou funções), caracterizando o paradigma procedu- ral, ou podemos ir além, criando uma modelagem complexa de todas as entidades envolvidas em um problema através das classes e objetos, e as instruções ficam encapsuladas como ações dos objetos (ou métodos), caracterizando a programa- ção orientada a objeto. Alternativamente, nossos programas podem ser declarativos: ao invés de instruções sequenciais, eles podem ser formados por funções matemáticas rigorosamente definidas. Essas funções podem, 39 inclusive, gerar outras funções, graças a recursos como as funções lambda. Isso caracteriza a pro- gramação funcional, extremamente determinística e segura. Alternativamente, as “declarações” podem ser predicados lógicos, e o programa em execução pode avaliar a validade de novas inferências a partir desses predicados, a chamada programação lógica. Muitas vezes um programa pode combinar ele- mentos de diferentes paradigmas – por exemplo, funções definidas com todo o rigor e recursos da programação funcional, mas um bloco principal de instruções imperativas que irá interagir com essas funções. Em alguns contextos específicos, um programa em qualquer um desses paradigmas poderá tam- bém incorporar elementos de outros paradigmas relacionados a restrições ou especificações de casos específicos de uso. Por exemplo, um pro- grama escrito em diferentes paradigmas pode incorporar conceitos da programação orientada a eventos, ficando preparado para tratar diferentes ocorrências, como uma interação do usuário como uma interface gráfica (um clique em um botão, por exemplo). De maneira geral, o paradigma a ser adotado deve ser suportado pela linguagem, pois frequentemente sua implementação bem-sucedida dependerá de 40 recursos que podem ou não ser oferecidos pela linguagem. A linguagem C, uma das linguagens procedurais mais conhecidas, não pode ser utili- zada para a programação orientada a objeto, por exemplo, já que não oferece os recursos clássicos desse paradigma, como objetos, classes, herança. Tampouco pode ser utilizada para a programação funcional, já que não oferece recursos como as funções lambda, limitando muito o que uma função pode ou não fazer. Em outros casos, não basta a linguagem oferecer um recurso: o próprio sistema operacional deverá oferecer esses recursos, e a linguagem será, na prática, uma interface para acessarmos esses recursos do sistema. É o caso da programação concorrente, ou programação paralela, quando desenhamos nosso programa para subdividir suas instruções em diferentes tarefas que podemser executadas de maneira simultânea por diferentes processadores. Essa subdivisão pode gerar dife- rentes problemas, e precisaremos de recursos adequados para tratá-los e garantir que estamos nos beneficiando de verdade dessa técnica, e não criando problemas graves que podem resultar em travamentos ou erros de cálculo. Um bom programador não deve ter linguagens ou paradigmas “de estimação”. Uma linguagem de programação deve ser vista como uma ferramenta. 41 Tanto o martelo quanto o alicate são ferramentas úteis, mas cada uma é muito boa em sua tarefa e pouco útil para realizar a tarefa da outra. O mesmo ocorre com linguagens, e é estudando diferentes paradigmas e nos aventurando em diferentes linguagens que conseguiremos perceber na prá- tica as vantagens e desvantagens de cada uma e desenvolver o espírito crítico necessário para escolher qual delas utilizaremos em cada um de nossos projetos futuros. Referências Bibliográficas & Consultadas CORREA, A. G. D. Programação I. São Paulo: Pearson, 2015. [Biblioteca Virtual]. FELIX, R. (Org.). Programação orientada a objetos. São Paulo: Pearson, 2016. [Biblioteca Virtual]. LEAL, G. C. L. Linguagem, programação e banco de dados: guia prático de aprendizagem. Curitiba: Intersaberes, 2015. [Biblioteca Virtual]. SANTOS, M. G. dos; SARAIVA, M. de O.; GONÇALVES, P. de F. Linguagem de programação. Porto Alegre: SAGAH, 2018. [Minha Biblioteca]. SEBESTA, R. W. Conceitos de linguagens de programação. 11. ed. Porto Alegre: Bookman, 2018. [Minha Biblioteca]. SILVA, E. A. da. Introdução às linguagens de programação para CLP. São Paulo: Blucher, 2016. [Biblioteca Virtual]. SILVA, F. M. da; LEITE, M. C. D.; OLIVEIRA, D. B. de. Paradigmas de programação. Porto Alegre: SAGAH, 2019. [Minha Biblioteca]. TUCKER, A.; NOONAN, R. Linguagens de programação: princípios e paradigmas. 2. ed. Porto Alegre: AMGH, 2014. [Minha Biblioteca]. Introdução Paradigmas de programação Programação Imperativa Programação Orientada a Objeto Programação Funcional Programação Lógica Programação Orientada a Eventos Programação Concorrente Considerações finais Referências Bibliográficas & Consultadas