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 +

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

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]
  • 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

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'

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
  • 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
  • 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

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

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;
  • 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:

    1. int soma(int a, int b)

    2. 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:

    1. int fibonacci(int n);

    2. 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