Paradigmas de Programação


Playlists

1 Paradigmas de Programação

Definição: estilo de programação, a forma como você descreve a solução computacional de um problema.

  • Muitos cursos de Computação e Engenharia iniciam com paradigma imperativo e estruturado (vide Processamento da Informação e Programação Estruturada).

  • Exemplo clássico da receita de bolo (que não é a melhor forma de descrever o conceito de algoritmo).

Alguns dos paradigmas mais populares:

  • Imperativo: um programa é uma sequência de comandos que alteram o estado atual do sistema até atingir um estado final.

  • Estruturado: programas com uma estrutura de fluxo de controle e uso de procedimento e funções.

  • Orientado objeto: organização através de objetos que contém dados, estados próprios e métodos que alteram ou recuperam os dados/estados. Os objetos comunicam entre si para compor a lógica do programa.

  • Declarativo: especifica o que você quer, mas sem detalhar como fazer.

  • Funcional: programas são avaliações de funções matemáticas sem alterar estados e com dados imutáveis.

  • Lógico: especifica-se um conjunto de fatos e regras, o interpretador infere respostas para perguntas sobre o programa.

Muitas das linguagens de programação são, na realidade, multi-paradigmas

  • Contêm características de diversos paradigmas
  • Contudo, na prática, elas acabam favorecendo um paradigma específico o que acaba lhes conferindo o título de linguagem “funcional” ou “imperativa” por exemplo

1.1 Paradigma Imperativo

  • Descreve-se passo a passo o que deve ser feito.
  • Infame goto.
  • Evoluiu para procedural e estruturado com if, while, for, …
    aprovados = {};
    i = 0;
inicio:
    n = length(alunos);
    if (i >= n) goto fim;
    a = alunos[i];
    if (a.nota < 5) goto proximo;
    nome = toUpper(a.nome);
    adiciona(aprovados, nome);
proximo:
    i = i + 1;
    goto inicio;
fim:
    return sort(aprovados);

😀 O fluxo de controle é explícito e completo, nada está escondido.

😀 Linguagem um pouco mais alto nível do que a linguagem de máquina.

😞 Ao seguir o passo a passo, você chega no resultado… mas pode ser difícil racionalizar qual será ele.

😞 Qualquer um pode usar suas variáveis globais e suas funções, para o seu uso intencional ou não…

1.2 Paradigma Estruturado

  • Estrutura o código imperativo em blocos lógicos.
  • Esses blocos contém instruções imperativas que, quando escritas, foram feitas para cumprir um único objetivo.
  • Elimina o uso de goto.
    • Ou deveria ter eliminado. Contra-exemplos: Java, C, …
aprovados = {};
for (i = 0; i < length(alunos); i++) {
    a = alunos[i];
    if (a.nota >= 5) {
	adiciona(aprovados, toUpper(a.nome));
    }
}
return sort(aprovados);

😀 O programa é dividido em blocos lógicos, cada qual com uma função explícita (se for bom programador).

😀 Estimula o uso de variáveis locais pertencentes a cada bloco lógico.

😞 Não evita que certas informações sejam utilizadas fora do seu contexto.

😞 Usa mudança de estados, o que pode levar a bugs.

1.3 Orientação a Objetos

  • Encapsula os dados em classes cada qual contendo seus próprios estados e métodos, não compartilhados.

  • Um objeto pode se comunicar com outro, não usa (ou evita) variáveis globais.

    • Linguagens como Smalltalk usam o termo troca de mensagens enquanto em outras como Java fala-se de chamadas/invocação de métodos
  • Métodos são divididos em métodos de instância , que lidam com o estado e manipulação de um objeto específico e métodos de classe/estáticos que, tipicamente, tratam requisições que não são particulares a um objeto

class Aprovados {
   private ArrayList aprovados;
   public Aprovados () {
       aprovados = new ArraList();
   }
   public addAluno(aluno) {
       if (aluno.nota >= 5) {
	   aprovados.add(aluno.nome.toUpper());
       }
   }
   public getAprovados() {
       return aprovados.sort();
   }

}

😀 Encapsula códigos imperativos para segurança e reuso.

😀 Evita o uso de variáveis globais, inibe uso indevido de trechos de códigos.

😞 Não são todos os problemas que podem ser facilmente ou naturalmente modelados como objetos.

😞 Composição de funções é feita através de herança, que pode bagunçar o fluxo lógico do código.

😞 Uso pesado de estados mutáveis.

😞 Difícil de paralelizar1.

1.4 Paradigma Declarativo

  • O fluxo lógico é implícito.
  • Separação clara entre o que o programador deseja obter do como proceder para obter o que ele deseja
  • Linguagens de alto nível que permite aos programadores dizer apenas o que desejam
SELECT   UPPER(nome)
FROM     alunos
WHERE    nota >= 5
ORDER BY nome

😀 Utiliza uma linguagem específica para o domínio da aplicação (Domain Specific Language - DSL), atingindo um nível mais alto que outros paradigmas.

😀 Minimiza uso de estados, levando a menos bugs.

😞 Fazer algo não suportado nativamente na linguagem pode levar a códigos complexos ou uso de linguagens de outros paradigmas.

😞 Pode ter um custo extra no desempenho.

1.5 Paradigma Lógico

  • Especifica-se apenas fatos e regras de inferência .
  • O objetivo (retorno) é escrito em forma de pergunta.
aprovado(X) :- nota(X,N), N>=5.

sort(
  findall(Alunos, aprovado(Alunos), Aprovados)
    )

😀 O compilador constrói o programa para você, baseado em fatos lógicos.

😀 Provar a corretude do programa é simples.

😞 Algoritmos mais complexos podem ser difíceis de expressar dessa forma.

😞 Costuma ser mais lento para operações matemáticas.

1.6 Paradigma Funcional

  • Baseado no cálculo λ .
  • Programas são composições de funções .
  • Não utiliza estados.
  • Declarativo.
sort [map toUpper (nome aluno) | aluno <- alunos, nota aluno >= 5]

😀 Expressividade próxima de linguagens declarativas, mas sem limitações.

😀 Não existe estado e mutabilidade, isso reduz a quantidade de bugs.

😞 Como fazer algo útil sem estados?

😞 A ausência de mutabilidade dificulta o gerenciamento de memória, intenso uso de Garbage collector.

2 Paradigma Funcional

  • Funções puras
  • Recursão
  • Avaliação Preguiçosa

Um Efeito colateral ocorre quando uma função altera algum estado global do sistema:

  • Alterar uma variável global
  • Ler entrada de dados
  • Imprimir algo na tela

2.1 Funções Puras

Funções puras são funções que não apresentam efeito colateral.

Ao executar a mesma função com a mesma entrada sempre se obtém a mesma resposta.

Se não temos efeito colateral…

  • …e o resultado de uma expressão pura não for utilizado → não precisa ser computado.
  • …o programa como um todo pode ser reorganizado e otimizado.
  • …é possível computar expressões em qualquer ordem (ou até em paralelo).
double dobra(double x) {
    return 2 * x;
}

2.2 Funções Impuras

double i = 0;

double dobraMaisI(double x) {
    i += 1;
    return 2 * x + i;
}

2.3 Pergunta

Classifique as seguintes funções em puras ou impuras:

  • strlen
  • printf
  • memcpy
  • getc

2.4 Resposta

  • strlen: pura
  • printf: impura
  • memcpy: pura
  • getc: impura
double media (int *valores, int n) {
    double soma = 0;
    int i;
    for (i = 0; i < n; i++)
	soma_valor(&soma, valores[i]);
    return soma / n;
}

void soma_valor (double *soma, int valor) {
    *soma += valor;
}
  • Funções impuras são virais!
    • Se sua função chama uma função impura, então sua função é impura

Pergunta: Se sua função só chama funções puras, então ela é pura?
Pergunta 2: Um programa que contenha apenas funções puras é útil?

Programação sem bugs
\(\cdot\) A ausência de estados permite evitar muitos erros de implementação.
\(\cdot\) O lema da linguagem Haskell: “se compilou, o código está correto!” (e não só pela pureza).

2.5 Iterações vs Recursões

  • Em linguagens funcionais os laços iterativos são implementados via recursão
    • Geralmente leva a um código enxuto e declarativo.
int mdc (int m, int n) {
  while (n != 0) {
    int r = m % n;
    m = n;
    n = r;
  }
  return m;
}
mdc 0 b = b
mdc a 0 = a
mdc a b = mdc b (a `rem` b)

2.6 Avaliação Preguiçosa

  • Algumas linguagens funcionais implementam o conceito de avaliação preguiçosa .

  • Quando uma expressão é gerada, ela gera uma promessa de execução ou _ thunk _.

  • Se e quando for necessário, o thunk é avaliado.

2.7 Avaliação Estrita

int main () {
    int x = 2;
    printf("%d\n", f(x * x, 4 * x + 3));
    return 0;
}

int f(int x, int y) {
    return 2 * x;
}
int main () {
    int x = 2;
    printf("%d\n", f(2 * 2, 4 * 2 + 3));
    return 0;
}

int f(int x, int y) {
    return 2 * x;
}
int main () {
    int x = 2;
    printf("%d\n", f(4, 4 * 2 + 3));
    return 0;
}

int f(int x, int y) {
    return 2 * x;
}
int main () {
    int x = 2;
    printf("%d\n", f(4, 11));
    return 0;
}

int f(int x, int y) {
    return 2 * x;
}
int main () {
    int x = 2;
    printf("%d\n", 8);
    return 0;
}

int f(int x, int y) {
    return 2 * x;
}

2.8 Avaliação Preguiçosa

f x y = 2 * x

main = do
  let z = 2
  print (f (z * z) (4 * z + 3))
f x y = 2 * x

main = do
  let z = 2
  print (2 * (z * z))
f x y = 2 * x

main = do
  let z = 2
  print (8)

A expressão 4 * z + 3 nunca foi avaliada!

Isso permite a criação de listas infinitas:

[2 * i | i <-[0..]]

2.9 Paradigma Funcional e as Linguagens de programação

Muitas linguagens de programação estão incorporando elementos de paradigma funcional por conta de seus benefícios.

O Python possui alguns elementos do paradigma funcional:

anonima = lambda x: 3*x + 1
par     = lambda x: x%2 == 0

map(anonima, lista)
filter(par, lista)

def preguicosa(x):
    for i in range(x):
	yield anonima(x)

Assim como a linguagem Java.

public interface List<E> {
    void add(E x);
    Iterator<E> iterator();
}

array.stream()
     .filter(n -> (n % 2) == 0);

Neste curso, concentraremos nossa atenção em Haskell:

  • Linguagem puramente funcional (não é multi-paradigma)
  • Somente aceita funções puras (como tratar entrada e saída de dados?)
  • Declarativa
  • Avaliação Preguiçosa
  • Dados imutáveis
  • Tipagem estática e forte

3 Disclaimer

Estes slides foram preparados para os cursos de Paradigmas de Programação e Desenvolvimento Orientado a Tipos na UFABC.

Este material pode ser usado livremente desde que sejam mantidos, além deste aviso, os créditos aos autores e instituições.


  1. Este é um ponto controverso já que a orientação a objetos surgiu como uma solução para concorrência (e não paralelização) em Simula. Neste contexto nos referimos às condições de corrida que são inerentes à POO mas que são inexistentes no paradigma funcional, por exemplo ↩︎