1ª parte: Primeiro contacto com a linguagem de programação OCaml e o seu ambiente de desenvolvimento na consola.
Exercícios de 1 a 8.
2ª parte: Exprimir ou aproximar os mecanismos disponíveis numa dada linguagem de programação numa outra linguagem que os não possua como primitivos.
Exercícios 9 e 10.
Nas aulas práticas o nosso ambiente de referência, tanto para trabalhar, como para os projetos, será o Linux instalado nos laboratórios (Ubuntu 16.04 - 64 bits). Destaque para o facto dos projetos de LAP serem corrigidos e avaliados no Linux dos laboratórios - o que significa que, no mínimo, é importante testar a versão final dos projetos no Linux, antes de submeter.
Ninguém está proibido de usar no seu computador pessoal o sistema operativo que quiser, e até existem versões para Windows e Mac de todo o software usado em LAP. Simplesmente, em LAP não é dado suporte a outros sistemas operativos, por exemplo para ajudar a resolver problemas de instalação (que por vezes se tornam particularmento complicados no Windows). Mas você poderá usar o fórum da disciplina para partilhar experiências com os seus colegas.
Manter três janelas abertas, no computador:
O processo de desenvolvimento de programas OCaml é o seguinte:
A linguagem de programação OCaml é um dialeto da linguagem de programação ML.
Na 1ª parte da cadeira LAP, vamos explorar a vertente funcional da linguagem OCaml. Em OCaml, um programa é essencialmente uma sequência de definições de constantes e de definições de funções. Constantes e funções são conceitos já conhecidos da matemática. Note que numa linguagem funcional pura não estão disponíveis, nem variáveis mutáveis, nem ciclos.
Um programa OCaml pode ser executado de duas formas distintas: (1) interpretado; ou (2) compilado e executado autonomamente. Um interpretador é um programa que lê o texto do programa e o executa diretamente. Um compilador transforma o texto do programa numa linguagem mais simples para mais tarde ser executado numa máquina virtual ou diretamente no hardware.
Para instalar o OCaml no Ubuntu usa-se o comando:
sudo apt install rlwrap ocaml-interp ocamlbuild
Para executar o interpretador na linha de comando do Ubuntu pode usar-se o comando:
rlwrap ocaml
O interpretador não tem suporte para a "história" dos comandos introduzidos previamente, mas o comando rlwrap resolve o problema.
Nestes primeiros exercícios vamos fazer diversas experiências usando o interpretador de OCaml, e também uma pequena experiência usando o compilador de OCaml.
Note que todas as entradas do interpretador são finalizadas com um ponto e vírgula duplo.
Experimente introduzir o seguinte e observe e interpete os resultados produzidos:
# 1+2;;Geralmente, é dado um valor como resposta, sendo ainda indicado o seu tipo e o identificador associado no caso de ser uma definição.
- : int = 3
# let x = 1;;
val x : int = 1
# let y = x*2+1;;
val y : int = 3
# #quit;;
Nota: Em vez da diretiva #quit;;, pode inserir CTRL-D no Linux ou CTRL-Z no Windows.
Experimente isto:
# print_string "Hello World!";;
Hello World!- : unit = ()
# print_int (10+3);;
13- : unit = ()
# print_char 'c';;
c- : unit = ()
# print_float 1.0;;
1.- : unit = ()
Usando um editor de texto, crie um ficheiro de texto chamado hello.ml, escreva o miniprograma (sim, só com uma linha)
print_string "Hello World!\n";;e depois compile-o na linha de comando assim:
$ ocamlc hello.ml -o hello
Este comando produz um ficheiro executável chamado hello na diretoria corrente. Execute-o assim:
$ ./hello Hello World!
Note que o interpretador escreve automaticamente o valor das expressões avaliadas na consola, mas o compilador não faz isso. Num programa que se destine a ser compilado, temos de ser nós a mandar escrever tudo usando as operações de input/output exemplificadas no exercício anterior.
# #use "hello.ml";; Hello World! - : unit = ()
Observe o resultado obtido e tire conclusões.
# let f x = x+1;; val f : int -> int = <fun> # f 2 ;; - : int = 3
Note que não se escrevem os tipos das entidades e que o interpretador de OCaml infere o seguinte tipo para a função: int -> int.
A seguinte função anónima
# fun x -> x+1;;
- : int -> int = <fun>
tem o mesmo tipo, mas não fica identificada por nenhum nome. É possível usar diretamente funções anónimas em expressões, como no exemplo:
# (fun x-> x+1) 2;;A nossa definição da função inicial f, também pode ser escrita assim:
- : int = 3
# let f = (fun x -> x+1);;
# let rec fact x = if x = 0 then 1 else x * fact (x-1) ;; val fact : int -> int = <fun> # fact 5 ;; - : int = 120
# let f x = x + x ;; # let f x = x +. x ;; # let f x = x ^ x ;; # let f (x,y) = x + y ;; # let f (x,y) = x +. y ;; # let f (x,y) = (y, x) ;; # let f (x,y) = x = y ;; # let g x y = x + y ;;
Este tipo de exercícios são muito instrutivos pois ajudam a entender melhor as linguagens envolvidas e dos seus mecanismos. O esforço, nada trivial, de resolver este tipo de exercícios, obriga a perceber completamente o mecanismo da linguagem L2 que se quer imitar, assim como os mecanismos da linguagem L1 que são usados na imitação.
Segue-se alguns exercícios deste género.
A função calcula o máximo divisor comum entre dois números inteiros positivos.
int mdc(int m, int n) { int x;Nota: Neste exercício, o ponto de partida foi um método escrito usando os mecanismos imperativos do Java. Descobre-se que é possível simular esses mecanismos em OCaml. Contudo, como veremos já a partir da próxima semana, quando se resolve um problema em OCaml, não há necessidade, sendo até prejudicial, pensar em termos de variáveis imperativas e de ciclos. Neste caso, estamos perante um exercício académico de "imitação dos mecanismos de outra linguagem".
while( n != 0 ) { x = n; n = m % n; m = x ;
} return m; }
interface Shape { double area(); } class Line implements Shape { private double x0, y0; private double x1, y1; double area() { return 0; } } class Circle implements Shape { private double x, y; private double radius; double area() { return 3.14159 * radius * radius; } } class Rect implements Shape { private double x0, y0; private double x1, y1; double area() { return Math.abs((x0 - x1) * (y0 - y1)); } } class Canvas { private java.util.Vector<Shape> shapes; double area() { double sum = 0; for(Shape s : shapes) sum += s.area(); return sum; } }Como você sabe, uma variável de tipo Shape consegue referir objetos de tipo Line, Circle e Rect. Ao aplicar a operação "area" a essa variável, obtém-se a área da figura geométrica em causa.
Agora, imagine que eram retirados do Java os mecanismos de herança e subtipo, que aqui são usados. Como é que reescreveria o programa anterior, tentando capturar o conceito da classe "Shape" (que representa uma forma geométrica geral) e, já agora, sem alterar a classe cliente Canvas? [Sugestão: a melhor solução mantém as cinco classes, sendo a classe Shape que precisa duma grande reescrita.] Já agora, complete o código, pois faltam os construtores.
Mas há uma situação rara em que o uso do goto é recomendado, se estiver disponível. Trata-se da programação de autómatos finitos deterministas (máquinas de estados). Num tal autómato, para cada par (estado, input) existe uma única transição para o estado seguinte. Você vai estudar este assunto na disciplina de Teoria da Computação.
O seguinte diagrama representa um autómato finito com 5 estados que verifica se o input contém, algures lá no meio, a string "foo". Partindo do estado "START", navege mentalmente do diagrama para perceber como ele funciona. [Nota: há um detalhe que foi simplificado relativamente ao que se estuda em TC.]
O programa mais abaixo, implementa em C o autómato anterior. Olhe para o código e repare que o goto permite transitar de estado de forma natural e intuitiva.
O problema a resolver é o seguinte: Escreva um programa em Java equivalente ao programa em C e teste-o.
O Java não tem instrução goto. Qual será o melhor plano para efetuar a tradução? Será que existe alguma solução que permita manter a estrutura geral do código C que estamos a ver? [A resposta é sim! Se fosse não, então o goto faria falta em Java.]
#include <stdio.h> int main() { START: switch( getchar() ) { case 'f' : goto F ; case EOF : goto FAIL ; default : goto START ; } F: switch( getchar() ) { case 'o' : goto FO ; case 'f' : goto F ; case EOF : goto FAIL ; default : goto START ; } FO: switch( getchar() ) { case 'o' : goto SUCCESS ; case 'f' : goto F ; case EOF : goto FAIL ; default : goto START ; } FAIL: printf("REJECT.\n") ; return 0 ; SUCCESS: printf("ACCEPT.\n") ; return 0 ; }Para ler carateres, pode usar o seguinte método, que imita a função getchar do C:
private static int getchar() { try { return System.in.read() ; } catch(IOException e) { return -1 ; } }
import java.util.*; // Este e' um tipo-objecto: interface IntStack { int capacity() ; int size() ; boolean empty() ; boolean full() ; int top() ; void push(int v) ; void pushN(int... vs) ; void pop() ; void popN(int n) ; } // Esta e' uma implementacao do tipo-objecto anterior: class IntStackClass implements IntStack { private final static int defaultCapacity = 100 ; private int[] elems ; private int top ; IntStackClass(int capacity) { elems = new int[capacity] ; top = 0 ; } IntStackClass() { this(defaultCapacity) ; } public int capacity() { return elems.length ; } public int size() { return top ; } public boolean empty() { return size() == 0 ; } public boolean full() { return size() == capacity() ; } public int top() { if( empty() ) throw new EmptyStackException() ; return elems[top-1] ; } public void push(int v) { if( full() ) throw new StackOverflowError() ; elems[top++] = v ; } public void pushN(int... vs) { for(int v : vs) push(v) ; } public void pop() { if( empty() ) throw new EmptyStackException() ; top-- ; } public void popN(int n) { for( int i = 0 ; i < n ; i++ ) pop() ; } // TESTING public static void main(String[] args) { System.out.println("Welcome to the IntStackClass class!!") ; IntStack s = new IntStackClass() ; System.out.println(s.capacity()) ; try { s.pushN(1,2,3,4,5) ; } catch ( StackOverflowError e ) { System.out.println("Stack Overflow!") ; System.out.println("Bye bye!") ; System.exit(1) ; } catch ( EmptyStackException e ) { System.out.println("Stack Underflow!") ; System.out.println("Bye bye!") ; System.exit(1) ; } s.pushN(1,2,3,4,5) ; s.popN(9) ; System.out.println(s.top()) ; } }Ajuda: Pode usar a seguinte classe enumerada auxiliar. Há uma solução baseada em ideias simples que passa por inserir no código alguns ifs, alguns returns, criar uma nova função main com código fixo, e pouco mais. Mesmo assim é uma solução complicada de usar, na prática.
public enum Exc { // Enumeration values noneExc, stackOverflowExc, emptyStackExc ; // Current exception private static Exc curr = noneExc ; // Static methods public static Exc Catch() { Exc x = curr ; curr = noneExc ; return x ; } public static void Throw(Exc e) { curr = e ; } public static boolean Throwed() { return curr != noneExc ; } public static void DefaultHandling() { switch( curr ) { case noneExc : break ; case stackOverflowExc: System.err.println("StackOverflow Exception") ; System.exit(0) ; case stackUnderflowExc: System.err.println("StackUnderflow Exception") ; System.exit(0) ; default: System.err.println("Unknown Exception") ; System.exit(0) ; } } }
int f(int[] array) { int sum = 0; for(int v : array) sum += v; return sum; }
(Não existe gravação da 2ª parte da aula, que decorreu no Zoom.)