Conceitos de Linguagens de Programação
MATA56 - Paradigmas de Linguagens de Programação
Universidade Federal da Bahia
Prof. Rodrigo Rocha <rodrigo@dcc.ufba.br>
Formas de implementação
-
Compilada: código-fonte => código nativo
- Ex.: .c => .exe
-
Interpretada: código-fonte
- Ex.: Python, Ruby
-
Híbrida: código-fonte => representação intermediária
- Ex.: .java => .class (bytecode)
- Ex.: .py => .pyc
É uma característica da implementação da linguagem. Ex.: existem compiladores e interpretadores de C.
Responsabilidades
- Um compilador/interpretador deve:
- reconhecer código-fonte válido
- informar erros no código-fonte
- gerar código executável ou intermediário
Sintaxe
Sintaxe
- Se refere à forma de escrever código-fonte válido na linguagem de programação
- É o que diz se uma linguagem de programação vai ser bonita (legível, fácil de escrever...)
Pascal
if c then begin b1 end else begin b2 end
C
if (c) { b1; } else { b2; }
LISP
(if c b1 b2)
Haskell
e | c = b1 | c2 = b2 | otherwise = b3
Fonte: http://rigaux.org/language-study/syntax-across-languages/CntrFlow.html#CntrFlowIfTheEls
código-fonte
(sintaxe concreta)
análise
léxica
sintática
árvore de sintaxe abstrata
(AST)
gerador
código-objeto
frontend
backend
Sintaxe concreta e abstrata
- Sintaxe concreta: detalhamento caractere a caractere de como escrever programas
- if (expr) { ... }
- Sintaxe abstrata: descreve a estrutura do programa (geralmente por meio de uma árvore)
Analisador léxico (lexer)
- Identifica os elementos da linguagem presentes no código-fonte.
- Converte uma sequência de caracteres em uma sequência de tokens
- Ex.: i, f, (, n, u, m, =, =, 1, ) ==>
INSTR_IF, ABRE_PARENTESES, IDENTIFICADOR (num), COMPARADOR_IGUALDADE, LITERAL_NUMERICO (1), FECHA_PARENTESES
Analisador léxico (lexer)
- A especificação dos tokens da linguagem é geralmente feita usando-se expressões regulares
- Ex.:
- LITERAL_NUMERICO: [0-9]+
- LITERAL_STRING: ".*?"
- COMPARADOR_IGUALDADE: ==
- IDENTIFICADOR: [a-zA-Z_][a-zA-Z0-9_]*
- INSTR_IF: if
Analisador sintático (parser)
- Um parser obtém como entrada uma sequência de tokens (saída do lexer) e gera uma árvore de sintaxe abstrata de acordo com uma gramática (geralmente escrita no formato BNF ou EBNF)
- Ex.:
- STMT_IF ::= INSTR_IF, ABRE_PARENTESES, EXPR, FECHA_PARENTESES, STMT
- ATRIBUICAO ::= IDENTIFICADOR, OP_ATRIB, EXPR
- Nesse passo pode-se detectar erros no código
- Ex.: o programa "if if" é inválido, pois a gramática define que depois do if tem de vir um abre-parênteses.
Analisador sintático (parser)
- Árvore de sintaxe abstrata (abstract syntax tree, AST)
Analisador sintático (parser)
- Problema do else pendente
Semântica
Semântica
- Se refere à interpretação do significado de um programa
- Um programa pode ser sintaticamente bem-formado mas ser semanticamente inválido
- Algumas questões sobre semântica:
- O identificador "pi" é uma variável? uma função? uma constante?
- A variável x é declarada antes de usar?
- Existe código que nunca é executado?
- A expressão if true then s1() else s2() pode ser simplificada?
- Quais os tipos das variáveis na expressão a + b? O operador + está definido para esses tipos?
Identificadores
- Identificadores são nomes dados a vários elementos de um programa, tais como variáveis, constantes, tipos e funções
- Cada linguagem define suas regras para identificadores:
- número máximo de caracteres?
- diferencia maiúsculas e minúsculas?
- quais os caracteres permitidos?
- palavras-chave (if, while, for...) podem ser usadas como identificadores? Se não, dizemos que as palavras-chave são palavras reservadas.
- Ruby: variáveis começam com minúscula; constantes começam com maiúscula
Tabela de símbolos
- Estrutura de dados usada pelo interpretador ou compilador que guarda informações sobre os identificadores no código-fonte de um programa
outros atributos: posição na memória, tamanho (array, strings), número e tipo dos argumentos (funções)...
Variáveis
- Variável = célula de memória + identificador
- Assume um valor que pode mudar com o tempo
- Características: nome, endereço, valor, tipo, tempo de vida e escopo
L-value e R-value
- Considere a atribuição:
- x = x + 1;
- As duas ocorrências de x representam coisas diferentes:
- L-value (left-value): o primeiro x denota a posição de memória onde o resultado será armazenado
- R-value (right-value): o segundo x denota o conteúdo da memória antes do cálculo da expressão
- Em alguns compiladores, a expressão (x+1) = 5 resulta no erro de compilação "L-value required"
Vinculação
- Uma vinculação (amarração ou binding) é uma associação entre entidades de programação. Ex.:
- Variável e valor
- Ex.: a variável x tem valor 5
- Identificador e tipo
- Ex.: o identificador x é uma variável
- Símbolo matemático e operação correspondente
- Ex.: operação de soma e símbolo +
- Variável e valor
Tempo de vinculação
- Momento em que uma vinculação é feita. Pode ser:
- vinculação estática
- tempo de projeto de linguagem
- tempo de implementação da linguagem
- tempo de compilação
- tempo de carga
- vinculação dinâmica
- tempo de execução
- vinculação estática
Tempo de vinculação
- Exemplo (Java): count = count + 5;
- o tipo de count é vinculado em tempo de compilação
- o significado do operador + é vinculado em tempo de compilação (pode ser soma ou concatenação)
- o valor de count é vinculado em tempo de execução
Tempo de vida (variável)
- É o tempo em que uma variável fica vinculada a uma posição de memória
- Quanto ao tempo de vida, as variáveis podem ser
- estáticas
- stack-dinâmicas
- heap-dinâmicas
Variáveis estáticas
- São alocadas (i.e., vinculadas a uma posição de memória) antes do início da execução do programa e desalocadas somente com o término do programa.
- Ex.: variáveis static em C:
- void incrementa() {
- static int x = 0;
- x++;
- printf("%d\n", x);
- }
- Ex.: variáveis globais
- Vantagem: eficiência no acesso (posição fixa), só aloca uma vez
- Desvantagem: ocupa memória mesmo quando não está sendo usada
Variáveis stack-dinâmicas
- O armazenamento é vinculado (i.e., a variável é alocada) em tempo de execução, no momento da declaração da variável
- Todas as variáveis stack-dinâmicas compartilham uma região de memória, a pilha (stack, last in first out)
- Ex.: variáveis locais de funções em C (são alocadas no início da execução da função e desalocadas no final)
- int somatorio(int n) {
- int i, soma = 0;
- for (i = 0; i < n; i++) { soma += i; }
- return soma;
- }
Variáveis heap-dinâmicas
- Variáveis alocadas/desalocadas em tempo de execução, em uma área chamada heap
- Podem ser alocadas e desalocadas a qualquer momento, não precisar seguir last in, first out
- Flexível o suficiente para construir estruturas de dados dinâmicas (listas encadeadas, árvores etc.)
- Ex.: malloc/free em C, new/garbage collector em Java
- Riscos:
- ponteiro pendente: desaloca memória, mas ainda há ponteiro para aquela posição
- vazamento de memória: a memória ainda está alocada, mas nenhum ponteiro aponta para ela
- compartilhamento: dois ponteiros apontam para a mesma posição de memória
https://en.wikipedia.org/wiki/Memory_safety
Gerenciamento de memória (da heap)
-
Manual: programador é responsável por desalocar a memória alocada (ex. em C: malloc/dispose)
- Bom: maior controle
- Ruim: fácil cometer erros (ex.: ponteiro pendente)
-
Coleta de lixo (garbage collection): automaticamente libera memória que não está sendo usada pelo programa
- Bom: menor preocupação do programador
- Ruim: menor controle
Garbage collection por contagem de referência
- Cada variável tem um contador, que é incrementado quando é referenciada e decrementado quando deixa de ser referenciada. Ao atingir 0, é desalocada.
- Ex.: Swift: automatic reference counting (ARC)
- referências weak, strong e unowned
- Ex.: Objective-C 1.x: [var retain], [var release]
- Ex.: Swift: automatic reference counting (ARC)
- Risco: referências circulares
- O programador ainda precisa ter algum conhecimento do seu funcionamento para escrever um programa
Garbage collection por contagem de referência
Garbage collection por tracing
- Muitas vezes chamado simplesmente de garbage collection
- Periodicamente o garbage collector (GC) libera células de memória que não podem ser acessadas através de uma busca em grafo.
- O programador pode forçar a execução do GC
- Bom: o programador quase não precisa se preocupar com gerenciamento de memória
- Bom: evita ponteiros pendentes e certos vazamentos de memória
- Ruim: o GC pode ser demorado e causar "soluços" imprevisíveis na aplicação
Garbage collection por tracing
Escopo e ambiente de referenciamento
- O escopo de um nome é a região do programa no qual o nome é visível, isto é, pode ser referenciado
- Ex.: nomes de variáveis, nomes de funções
- Na maioria das linguagens, o escopo de uma variável é determinado estaticamente (i.e., antes da execução do programa)
Escopo
var glob = 123; // 1 console.log(glob); // 2 function teste() { // 3 var x = 2; // 4 console.log(x); // 5 } // 6 glob = 456; // 7
Qual o escopo da variável glob?
Qual o escopo da variável x?
Escopo
var glob = 123; // 1 console.log(glob); // 2 function teste() { // 3 var x = 2; // 4 console.log(x); // 5 } // 6 glob = 456; // 7
glob tem escopo global: é acessível de qualquer parte do programa, a princípio (existem exceções, como veremos)
x tem escopo local: só é acessível no bloco de código da função teste.
Ambiente de referenciamento
var glob = 123; // 1 console.log(glob); // 2 function teste() { // 3 var x = 2; // 4 console.log(x); // 5 } // 6 glob = 456; // 7
Qual é o ambiente de referenciamento da linha 7?
E da linha 5?
É o conjunto de nomes visíveis em uma região do programa.
/* 1 */ var glob = 1; /* 2 */ /* 3 */ function externa() { /* 4 */ var xext = 2; /* 5 */ /* 6 */ function interna() { /* 7 */ var xint = 3; /* 8 */ console.log(xext); /* 9 */ } /* 10 */ interna(); /* 11 */ } /* 12 */ externa();
JavaScript permite funções aninhadas ==> escopos aninhados.
O que vai acontecer ao executar a linha 8?
A variável xext faz parte do amb. de referenciamento da linha 8?
/* 1 */ var x = 1; /* 2 */ /* 3 */ function externa() { /* 4 */ var x = 2; /* 5 */ /* 6 */ function interna() { /* 7 */ var x = 3; /* 8 */ console.log(x); /* 9 */ } /* 10 */ interna(); /* 11 */ } /* 12 */ externa();
Na linha 8, o nome x se refere à variável declarada em qual linha?
/* 1 */ var x = 1; /* 2 */ /* 3 */ function externa() { /* 4 */ var x = 2; /* 5 */ /* 6 */ function interna() { /* 7 */ var x = 3; /* 8 */ console.log(x); /* 9 */ } /* 10 */ interna(); /* 11 */ } /* 12 */ externa();
A declaração de x na linha 4 mascara a variável x da linha 1 (sombreamento)
Qual o escopo das diferentes vinculações de x?
Tipos
- Um tipo (de dados) determina
- um conjunto de valores possíveis,
- as operações que podem ser feitas sobre o tipo
- o significado
- e a forma de armazenamento
- Ex.: unsigned short int (em C)
- valores: de 0 a 65535
- operações: +, -, *, /, %, ==, !=, <, <=, dentre outros
- significado: número inteiro
- armazenamento: 2 bytes
- a implementação define endianness (ordem dos bytes)
Tipos
- Podem ser:
- embutidos/predefinidos
-
customizados/definidos pelo usuário
- ex.: structs, enums, classes
- Podem ser:
-
primitivos
- ex.: int, float, char
-
compostos: definidos a partir dos primitivos
- ex.: string (char *), arrays, structs
- Ex. em Java: int é primitivo, Integer não
-
primitivos
Tipos
- Podem ser:
- numéricos (inteiro, ponto flutuante, decimal)
- booleanos
- caracter
-
ordinais (inclui os anteriores)
-
enumeração. Permite <, >, =. Ex. (Pascal):
- type colortype = (red, green, blue);
- enums em C e Java
-
subrange. subsequência de um tipo ordinal
- ex.: type tx = 1..10; upcase = 'A'..'B'
-
enumeração. Permite <, >, =. Ex. (Pascal):
Tipos
- Podem ser:
-
mutáveis: o valor pode ser alterado após inicialização
- Ex.: NSMutableArray (Objective C)
- Ex.: listas em JavaScript, Ruby, Python
-
imutáveis: o valor não pode ser alterado
- Ex.: String em Java
- Ex.: NSArray (Objective C)
- Ex.: tuplas em Python
-
mutáveis: o valor pode ser alterado após inicialização
- Atenção: um tipo imutável pode conter valores mutáveis (ex.: tuplas contendo listas)
Tipo String
- Vetor de caracteres ou primitivo
- Como calcular o comprimento?
- C usa byte 0 para indicar término - O(n)
- BASIC armazena contagem de caracteres - O(1)
- Pascal preenche string com espaços
- Concatenação de strings (A + B). Ex. (Java):
- "Não use + para concatenar strings!"
- Strings são imutáveis; A + B gera nova string
- StringBuffer: concatenação nem sempre gera nova alocação de memória
Tipo Array
- É um tipo composto homogêneo, i.e., os elementos são do mesmo tipo
- Os elementos são indexados
- C: a partir de 0
- R: a partir de 1
- Pascal: arbitrário (você escolhe)
- Podem ser uni ou multidimensionais (matrizes)
- Matrizes: row major vs column major order
- JavaScript, Python e Ruby possuem listas, que são tipos compostos heterogêneos (pode misturar elementos de tipos diferentes)
- Tipo set/conjunto: não admite elementos repetidos
Tipo array associativo
- Estrutura chave-valor
- Também chamado de dicionário, hash, mapa
- Ex.:
{"pedra": "ametista",
"cor": "amarelo",
"tamanho": 4}
Outros:
- Tipo registro (struct em C)
- Tipo composto heterogêneo
- Tipo union
- Tipo ponteiro, tipo referência
- Tipos genéricos (Java) / templates (C++):
- Ex.: Array<Integer>, HashMap<String, Integer>
Vinculação de tipos
-
Pode ser
-
estática. Os tipos das variáveis podem ser determinados sem executar o programa.
- ex.: Java, C
-
dinâmica. O tipo é definido em tempo de execução.
- ex.: Python, Ruby
-
estática. Os tipos das variáveis podem ser determinados sem executar o programa.
-
Pode ser
-
explícita. Os tipos são declarados.
- int x; String y;
-
implícita. Os tipos não são declarados.
- if (random()) x = a; else x = b. <== vinculação dinâmica
- x = 1. Em algumas linguagens pode-se inferir o tipo estaticamente, em tempo de compilação
-
explícita. Os tipos são declarados.
Tipificação: forte vs. fraca
- Ou tipagem. As definições são imprecisas
-
Forte: consegue detectar a maioria dos erros de tipo
- ex.: "3" + 2 (tipo string + tipo inteiro)
- ex.: int *x; float *y; x = y; ... *x; (ponteiro)
- ex.: char x[10], y[20]; strcpy(x, y); (buffer overflow)
- ex.: printf("%s", 12)
- ex.: int x = 3.5 + 1; (truncamento)
- ex.: acesso de ponteiro nulo
- Fraca: contrário de forte
- Note que uma linguagem pode ter tipificação dinâmica e forte. Ex.:
- a = "3"
- b = 2
- c = a + b <== erro em tempo de execução
Tipificação: forte vs. fraca
- Gotchas
- JavaScript
- http://www.codeproject.com/Articles/182416/A-Collection-of-JavaScript-Gotchas
- http://brian.io/slides/dotjs-2012/
- "3" + 2
- "3" - 2
- 0.1 + 0.2
- JavaScript
Conversão de tipos
- Às vezes é necessário converter o tipo de um dado (ex.: expressões com tipos mistos)
- Pode ser
-
explícita (cast). Ex.: int x; float y = (float)x / 2;
- Ex. (Ruby): 1.to_f / 2.0
- implícita (coerção). Ex.: int a = 1; float b = 0.5, c = a + b;
-
explícita (cast). Ex.: int x; float y = (float)x / 2;
- Pode ser
- ampliadora (widening, >) - conversão para um tipo que inclui (ao menos aproximadamente) o tipo original. Ex.: int => float
- redutora (narrowing, <) - o contrário. Ex.: float => int (perde informação!)
- Java: boxing (int => Integer) e unboxing (Integer => int)
Subrotinas
- Uma subrotina ou subprograma é uma sequência de instruções empacotadas como uma unidade
- A depender do contexto, são chamadas de procedimentos, funções, rotinas, métodos...
- Possuem um cabeçalho: nome, tipo de retorno, lista de parâmetros
Parâmetros
- Considere a declaração:
- int soma(int a, int b)
- a e b são parâmetros formais da função soma
- Considere a chamada:
- int x = soma(3, 4);
- 3 e 4 são parâmetros reais ou argumentos da chamada à função soma
Vinculação entre parâmetros e argumentos
-
A vinculação entre argumentos e parâmetros geralmente se dá por posição (1o parâmetro recebe o 1o argumento etc.). Ex.:soma(3, 4)
-
Algumas linguagens admitem parâmetros nomeados, de forma que os argumentos podem ser passados em qualquer ordem. Ex.:soma(b=4, a=3)
-
Algumas linguagens permitem parâmetros opcionais com valores default. Ex.: soma(b=4) ou soma(3).
-
Algumas linguagens permitem número variável de argumentos. Ex.: soma(1, 2, 3, 4, 5, 6, 7).
Passagem de parâmetros
-
Existem diversos mecanismos para passagem de parâmetros durante a chamada, de acordo com 2 dimensões:
-
o valor é transferido para a função (in) vs o valor é transferido da função para o chamador (out) ou ambos (inout)
-
o valor é copiado para a função vs. uma referência é passada para a função
-
Passagem de parâmetros
Passagem de parâmetros
-
Chamada por valor: o argumento é avaliado e copiado para a subrotina (in)
-
Chamada por referência: uma referência para o argumento é passada para a subrotina (in/out)
-
Chamada por valor-resultado: o argumento é copiado para a subrotina e, ao fim de sua execução, é copiado de volta para o chamador (in/out). Ex.: rede
Passagem de parâmetros
- C#: palavra-chave ref para passar por referência.
- Passagem por referência é mais rápida e usa menos memória para objetos grandes. E se eu quiser passar por referência mas não ter o comportamento in/out?
- C++: palavra-chave const
- C tem passagem por referência?
- Exemplo clássico de passagem por referência: função swap (troca).
- Java usa passagem por valor ou por referência? Exemplo de função que recebe uma lista e adiciona um elemento a ela.
Avaliação estrita vs preguiçosa
-
Todos esses casos são estratégias de avaliação estrita: os argumentos de uma função são avaliados antes da chamada da função.
-
Considere o seguinte exemplo em C:
-
1 + 1 == 2 ? printf("A") : printf("B");
-
Se pensarmos no operador ternário (?:) como uma função de três parâmetros, ela segue uma avaliação estrita?
-
ternario(1 + 1 == 2, printf("A"), printf("B"))
-
Como criar a função ternario em C?
-
Passagem de parâmetros
Avaliação não-estrita: os argumentos são avaliados apenas seu valor é requisitado. Aplicação: listas infinitas. Alguns tipos:
-
Chamada por nome: o texto do argumento é passado como parâmetro e avaliado dentro da função (in/out). Usado em Scala. Ver exemplo. #define em C. Aplicação: logging.
-
Chamada por necessidade: igual ao anterior, mas avalia parâmetro só uma vez (faz caching do resultado). Ex.: função fibonacci recursiva.
Funções como valores
-
Nas linguagens com suporte ao paradigma funcional, uma função é um valor (assim como números, strings etc.)
-
Nesse caso dizemos que funções são cidadãos de primeira classe
-
Isso significa que é possível
-
atribuir uma função a uma variável
-
passar uma função como parâmetro para outra
-
criar funções que retornam funções
-
-
Esse tópico será detalhado na unidade sobre programação funcional
Sobrecarga de subrotinas
A sobrecarga ocorre quando há duas ou mais funções com o mesmo nome dentro do mesmo ambiente de referenciamento
-
A função chamada depende da lista de parâmetros
número de parâmetros
tipo dos parâmetros
Sobrecarga de subrotinas
-
Considere as duas declarações a seguir:
-
int soma(int a, int b)
-
float soma(float a, float b)
-
-
Uma chamada a soma(3, 5) invocará qual das funções?
-
E se for soma(3, 5.0f)? E se for soma(x, y), onde x e y são do tipo double?
Sobrecarga de subrotinas
-
Considere as duas declarações a seguir:
-
int fibonacci(int n);
-
long fibonacci(int n);
-
-
Considere a seguinte chamada:
-
long x = fibonacci(10)
-
-
Qual das funções será chamada? E se a chamada for:
-
float x = fibonacci(10)
-
-
Em C, não há como distinguir funções somente pelo tipo de retorno.
Ecossistema da linguagem
- Bibliotecas e frameworks (ex.: rails, junit)
- Automação de build (ex.: make, ant)
- Gerenciamento de dependências (ex.: maven)
- Documentação de APIs (ex.: javadoc)
- Gerenciador de instalações (ex.: rvm)
- Outros
- Analisador de código (ex.: sonar)
- Integração contínua (ex.: jenkins, travis)
Conceitos de Linguagens de Programação
By Rodrigo Rocha Gomes e Souza
Conceitos de Linguagens de Programação
- 2,024