Linguagens e Ambientes de Programação (2022/2023)



Teórica 05 (16/mar/2023)

Tipos produto (tuplos e registos).
Tratamento de exceções.
Funções parciais.



Tipos produto (tuplos e registos)

A maioria das linguagens de programação incluem nos seus sistemas de tipos uma construção específica que permite representar agrupamentos de dados heterogéneos. O nome genérico dessa construção, independente da linguagem particular, é tipo produto.

Numa linguagem com tipos produto é possível definir, por exemplo, um tipo de dados pessoa constituído por um nome (de tipo string), um ano de nascimento (de tipo int), uma morada (de tipo string), etc.

Os tipos produto do Pascal são os registos.
Os tipos produto do C são as estruturas (mas também se podem designar como registos)
Os tipos produto do Java, Smalltalk e C++ são as classes.
No Fortran, os tipos produto apareceram na versão Fortran 90 com o nome de tipos derivados.

Como é em OCaml?

Tipos produto em OCaml

Em OCaml há duas variedades de tipos produto: tipos produto não etiquetados e tipos produto etiquetados.

Tipos produto não etiquetados (tuplos)

Os produtos cartesianos do OCaml são exemplos de tipos produto, neste caso ditos não-etiquetados, e também conhecidos por tuplos. Por exemplo, para representar pessoas, pode usar-se em OCaml o seguinte tipos produto não etiquetado:

Literais: Para exemplificar, eis um valor do tipo anterior: Construção: Para exemplificar vejamos uma função que muda a morada duma pessoa, criando um tuplo novo: Processamento: Como se processam tuplos? De duas formas:

Tipos produto etiquetados (registos)

Mas o OCaml, também suporta tipos produto etiquetados, também conhecidos como registos, os quais requerem definição explícita. Eis um exemplo de tipo produto etiquetado: Literais: Eis um literal deste tipo: Construção: Para exemplificar vejamos uma função que muda a morada duma pessoa, criando um registo novo: Processamento: Como se processam registos? De duas formas:

Tratamento de exceções em OCaml

Durante a execução de um programa, por vezes verificam-se determinadas condições (geralmente anómalas, mas nem sempre) às quais é necessário reagir alterando o fluxo de execução normal. Tais condições chamam-se exceções.

As exceções podem ser geradas:

Captura e tratamento de exceções

Quando uma exceção é gerada, o controlo da execução do programa é transferido para o tratador de exceções (exception handler) mais recentemente ativado. É abortada a avaliação de todas as funções chamadas depois da ativação desse tratador de exceções.

Em OCaml, um tratador de exceções escreve-se usando uma expressão try-with, como se exemplifica de seguida. A expressão exp, no seu interior, diz-se uma expressão protegida.

Como é avaliada uma expressão try-with? Existe um tratador de exceções de sistema que apanha as exceções não tratadas e aborta a execução do programa com uma mensagem de erro apropriada a cada caso. Exemplos:

Definição de novas exceções

A lista de exceções predefinidas na linguagem encontra-se aqui: Index of exceptions.

O programador pode definir novas exceções. A sintaxe da definição duma nova exceção é igual à sintaxe da definição duma variante dum tipos soma.

Exemplos. O primeiro exemplo define uma exceção com argumento; o segundo define uma exceção sem argumento.



Funções parciais

Uma função parcial é uma função que só está definida em parte do seu domínio. (Não confundir com aplicação parcial, que é outra coisa.)

Por exemplo, a função fact é parcial porque só está definida para valores não-negativos:

    let rec fact n = (* pre: n >= 0 *)
        if n = 0 then 1
        else n * fact (n-1)
    ;;

Outro exemplo: A função maxList é parcial porque só está definida para listas não-vazias:

    let rec maxList l = (* pre: l <> [] *)
        match l with
        | [x] -> x
        | x::xs -> max x (maxList xs)
    ;;
Quando se escreve uma função parcial, espera-se que essa função seja sempre aplicada a argumentos válidos. Mas os programas podem ter erros, como sabemos... Como lidar com esta situação?

Solução clássica: A solução clássica não verifica as precondições e passa a responsabilidade para quem chama a função. A função anuncia na precondição as suas restrições de uso. Estas restrições devem ser cumpridas pelo programador. Se o programador não cumprir as precondições, as consequências ficam a cargo do programador.

Solução do Java: Na linguagem Java, existe a convenção de que as precondições são verificadas à entrada de cada método. Deve ser lançada uma exceção sempre que uma precondição é violada. O programador tem a tarefa de escrever o código de validação, no início de cada método.

Solução do OCaml: Podemos verificar explicitamente as precondições para gerar exceções, mas também podemos não o fazer. Se não o fizermos, na maioria dos casos a linguagem OCaml já trata automaticamente do assunto, gerando exceções quando certas precondições são violadas.

Em OCaml, veja estes dois exemplos com verificação explícita, onde se gera uma exceção e uma mensagem de erro clara por cada precondição violada:

Mas, repare que, em OCaml, mesmo sem lançar exceções explícitas, as versões originais destas duas funções já geram exceções: a primeira aborta com "Stack overflow" e a segunda gera a exceção Match_failure.

Observação importante: É importante que os programas não disfarcem esses erros. É muito melhor a execução dum programar abortar de forma catastrófica, do que terminar de forma aparentemente normal, produzindo resultados errados. Por isso, convém garantir que uma função parcial não produz realmente qualquer resultado quando aplicada a argumentos inválidos. Gerar uma exceção ou entrar num ciclo infinito, são duas formas que garantem a não produção de resultado.



Vídeos antigos

Estes vídeos poderão estar um pouco desatualizados, pois foram feitos no contexto duma edição anterior do LAP. Contudo, partes dos vídeos poderão continuar a ter alguma utilidade.

#50 110