Numa linguagem funcional, a recursividade está na base da programação de todas as atividades repetitivas.
A programação de funções recursivas não é novidade pois já foi abordada em cadeiras anteriores, por exemplo POO e AED. Contudo, o estudo duma linguagem funcional é uma excelente oportunidade para revisitar o tema, com os seguintes objetivos: aprofundar a análise do mecanismo; estudar diversos padrões de utilização; passar a encarar a recursividade como uma técnica que ajuda a descobrir soluções para certos problemas complicados; ser exposto a uma forma de pensar diferente.
Nós queremos usar a recursividade para escrever programas que que enfatizem as propriedades lógicas dos problemas em vez de questões operacionais. O nosso objetivo, nesta primeira parte de LAP, é programar a alto nível, a um nível mais humano, evitando ter de pensar como uma máquina. Esta forma de programação é por vezes designada de programação declarativa. Alguns dos nossos programas serão certamente menos eficientes do que os correspondentes programas imperativos, mas a nossa capacidade para resolver problemas complicados aumentará.
let rec fact n = if n = 0 then 1 else n * fact (n-1) ;; let rec len l = match l with | [] -> 0 | x::xs -> 1 + len xs ;;Analisando estas duas funções recursivas verificamos que quando lidam com o caso não-trivial - o chamado caso geral - ambas efetuam a redução do problema original a uma instância mais simples do mesmo problema. Concretamente, a função len reduz o problema len (x::xs) ao problema mais simples len xs, e a função fact reduz o problema fact n ao problema mais simples fact (n-1).
Além de efetuarem redução de problemas no caso geral, as duas funções lidam também, separadamente, com o caso trivial de cada problema - o chamado caso base. Concretamente, a função len trata separadamente o caso base len [] e a função fact trata separadamente o caso base fact 0.
Repare que quando uma destas duas funções é aplicada a um argumento, inicia-se uma sucessão de reduções a problemas mais simples que só termina quando o caso base é alcançado. Por exemplo, a sequência de reduções produzida pela aplicação len [7;6;5;4] é a seguinte:
len [7;6;5;4] <-- problema inicial = 1 + len [6;5;4] <-- problema um pouco mais simples = 1 + 1 + len [5;4] <-- problema ainda mais simples = 1 + 1 + 1 + len [4] <-- problema ainda mais simples = 1 + 1 + 1 + 1 + len [] <-- caso base = 1 + 1 + 1 + 1 + 0 = 4
As funções recursivas len e fact são representativas do que é programar usando o núcleo funcional do OCaml. Em OCaml programa-se essencialmente reduzindo problemas a problemas mais simples.
O método indutivo consiste na seguinte técnica:
Resolução: Para começar, a função len recebe uma lista, a qual terá de ser processada usando emparelhamento de padrões. Aplicando o método indutivo ao caso geral G=len (x::xs), vamos começar por assumir que já temos o problema resolvido para o caso mais simples S=len xs.
Desde já podemos ir escrevendo a seguinte estrutura incompleta:
let rec len l = match l with | [] -> ... | x::xs -> ... len xs ... ;;O problema que temos para resolver é o seguinte: Como é que se obtém o valor de G=len (x::xs) a partir do valor de S=len xs? Mas este problema é simples! De facto, basta adicionar uma unidade a S para se obter G!
Preenchendo o que falta no esquema anterior, surge a solução final:
let rec len l = match l with | [] -> 0 | x::xs -> 1 + len xs ;;NOTA1: Neste exemplo, reduzimos o problema G=len (x::xs) ao problema S=len xs. Mas esta não é a única redução possível... Existem muitas outras... Voltaremos a este assunto noutra aula.
NOTA2: A função len é tão simples que faz o método indutivo parecer óbvio e desinteressante. No entanto, este método de resolver problemas tem imensas virtualidades:
Comprimento de lista:
len : 'a list -> int let rec len l = match l with | [] -> 0 | x::xs -> 1 + len xs ;;Soma dos elementos de lista:
sum : int list -> int let rec sum l = match l with | [] -> 0 | x::xs -> x + sum xs ;;Concatenação de listas:
append : 'a list -> 'a list -> 'a list let rec append l1 l2 = match l1 with | [] -> l2 | x::xs -> x::append xs l2 ;;Acrescentar elemento no final de lista:
putAtEnd : 'a -> 'a list -> 'a list let rec putAtEnd v l = match l with | [] -> [v] | x::xs -> x::putAtEnd v xs ;;Inversão de lista:
rev : 'a list -> 'a list let rec rev l = match l with | [] -> [] | x::xs -> append (rev xs) [x] ;;Máximo duma lista (tratamento implícito do erro):
maxList : 'a list -> 'a let rec maxList l = (* pre: l <> [] *) match l with | [x] -> x | x::xs -> max x (maxList xs) ;;Máximo duma lista (tratamento explícito do erro):
maxList : 'a list -> 'a let rec maxList l = (* pre: l <> [] *) match l with | [] -> failwith "maxList: lista vazia" | [x] -> x | x::xs -> max x (maxList xs) ;;Aplicação de função a todos os elementos de lista:
map : ('a -> 'b) -> 'a list -> 'b list let rec map f l = match l with | [] -> [] | x::xs -> (f x)::map f xs ;;Seleção dos elementos duma lista que verificam uma dada propriedade:
filter : ('a -> bool) -> 'a list -> 'a list let rec filter b l = match l with | [] -> [] | x::xs -> if b x then x::filter b xs else filter b xs ;;Concatenação de todas as listas contidas numa lista de listas:
flatten : 'a list list -> 'a list let rec flatten ll = match ll with | [] -> [] | l::ls -> l @ flatten ls ;;Concatenação de todos os resultados gerados por uma função de mapping que produz listas:
flatMap : ('a -> 'b list) -> 'a list -> 'b list let rec flatMap f l = match l with | [] -> [] | x::xs -> (f x) @ flatMap f xs ;;Ordenação de listas:
sortList : 'a list -> 'a list let rec insOrd v l = match l with | [] -> [v] | x::xs -> if v <= x then v::x::xs else x::insOrd v xs ;; let rec sortList l = match l with | [] -> [] | x::xs -> insOrd x (sortList xs) ;;Nota: Repare que não tinhamos pensado em nenhum algoritmo de ordenação em particular e que o método indutivo nos levou a inventar um algoritmo de forma não deliberada.
Fusão de listas ordenadas sem repetições (aqui dá jeito usar matching duplo):
fusion : 'a list -> 'a list -> 'a list let rec fusion l1 l2 = (* pre: l1, l2 sorted with no repetitions *) match l1, l2 with | _, [] -> l1 | [], _ -> l2 | x::xs, y::ys -> if x < y then x::fusion xs l2 else if y < x then y::fusion l1 ys else x::fusion xs ys ;;
sum [1;2;3] = 1 + sum [2;3] = 1 + 2 + sum [3] = 1 + 2 + 3 + sum [] = 1 + 2 + 3 + 0 = 6 append [1;2;0] [3;4] = 1::append [2;0] [3;4] = 1::2::append [0] [3;4] = 1::2::0::append [] [3;4] = 1::2::0::[3;4] = [1;2;0;3;4] rev [1;2;3] = (rev [2;3]) @ [1] = (rev [3]) @ [2] @ [1] = (rev []) @ [3] @ [2] @ [1] = [] @ [3] @ [2] @ [1] = [3;2;1]
[1;2;3] @ [4;5;6] = append [1;2;3] [4;5;6] = [1;2;3;4;5;6]No entanto, a função @ é pouco eficiente e deve ser usada com critério. Por exemplo, para acrescentar um zero à cabeça duma lista list é má ideia escrever [0]@list; escreve-se sempre 0::list. No entanto, para acrescentar um zero ao final duma lista list não temos alternativa e escreve-se mesmo list@[0].
let n = exp in exp1associa o nome n ao valor da expressão exp no contexto da expressão exp1. O nome n passa a definir uma constante local à expressão exp1.
O nome n também pode ser parametrizado, passando a denotar uma função local, assim
let n x = exp in exp1Esta forma é equivalente a
let n = (fun x -> exp) in exp1
A palavra reservada and pode ser usada para definir simultaneamente vários nomes no mesmo let-in. Por exemplo:
let a = 1 and b = 2 in a + b ;;ou
let f x = x + 1 and g x = x + 2 in f (g x) ;;Exercício: Procure no manual da linguagem a forma sintática geral da construção let-in.
Vamos ver de seguida que a construção let-in permite:
1ª hora 120 cêntimos 2ª hora 140 cêntimos 3ª hora 150 cêntimos seguintes 155 cêntimosApresentam-se a seguir duas versões equivalentes da função parque, a segunda programada usando let-in. A escolha entre elas é, em grande medida, uma questão de gosto, mas pode argumentar-se que a segunda versão, apesar de mais longa, é mais fácil de perceber porque torna explícita a ordem de avaliação das subexpressões dentro duma expressão que é demasiado grande e complexa.
let parque (he, me) (hs, ms) = pagar (convHoras (permanencia (convMinutos he me) (convMinutos hs ms))) ;;
let parque (he, me) (hs, ms) = let te = convMinutos he me in let ts = convMinutos hs ms in let perm = permanencia te ts in let h = convHoras perm in pagar h ;;Exercício: As funções auxiliares não foram ainda programadas. Tente escrevê-las.
let f g x = g x + g x * g x ;; let f g x = let r = g x in r + r * r ;;
let rec even n = if n = 0 then true else odd (n-1) and odd n = if n = 0 then false else even (n-1) in exp ;;O que fazem estas funções?
Se quisermos funções globais, então escrevemos (sem o in):
let rec even n = if n = 0 then true else odd (n-1) and odd n = if n = 0 then false else even (n-1) ;;
let n = exp in exp1é internamente traduzida para a seguinte representação:
(fun n -> exp1) expAs duas formas são equivalentes (medite um pouco nisso!), mas a primeira é mais fácil de perceber.
Por exemplo, a seguinte sequência de definições e expressões:
# let f x = x + 1 ;; # let g x = x + 2 ;; # f (g x) ;;é internamente traduzida para uma sequência de let aninhados:
let f x = x + 1 in let g x = x + 2 in f (g x) ;;