Recorde-se que programar imperativamente consiste em mandar o computador executar uma sequência de instruções para ir alterando os valores guardados na memória até se obter o resultado pretendido. Cada célula de memória (variável) pode guardar valores de um certo tipo. Por exemplo, já encontrámos variáveis de tipo integer que podem guardar valores inteiros. Para além dos tipos básicos primitivos disponibilizados pela linguagem F (integer, real, logical, ...) é possível ainda declarar variáveis de tipo tabular (vectores, matrizes,...). Põe-se naturalmente a questão de saber se será útil declarar e trabalhar com variáveis de outros tipos definidos por nós. De facto assim é! Mais ainda, um dos critérios mais importantes para avaliar quão eficaz é uma linguagem de programação consiste em analisar as facilidades da linguagem para definir novos tipos de objectos, isto é, novos tipos de variáveis de memória.
A linguagem F é particularmente rica neste aspecto como veremos de seguida. Mas vale a pena motivar desde já a importância de definir e trabalhar com objectos de tipos não primitivos, isto é, definidos por nós. Considere-se o problema (fictício) das torres de Hanoi (http://www.cs.wm.edu/~pkstoc/toh.html). Os monges de Hanoi acreditam que quando acabarem de transferir um a um os sessenta e quatro discos da torre 1 para a torre 2, usando a torre 3 como auxiliar, nunca sobrepondo um disco maior a um disco menor, o universo acaba por a humanidade ter concluído a tarefa para que foi criada.
torre 1 torre 2 torre 3
Pretendemos modelar esta tarefa em computador, representando cada torre por um objecto de tipo apropriado na memória do computador. O tipo relevante é um tipo bem comum que surge em inúmeros problemas computacionais. Trata-se do tipo pilha (stack). Os objectos deste tipo são referenciáveis e manipuláveis através das funções e subrotinas seguintes:
new() é uma função sem parâmetros que devolve a pilha vazia;
push(x,s) é uma subrotina que quando invocada sobrepõe o elemento x à pilha s; pop(s) é uma subrotina que quando invocada retira o elemento que está no topo da pilha s; top(s) é uma função devolve o topo da pilha s;
Com efeito cada torre pode ser vista como um objecto de tipo pilha de naturais (representando cada disco por um número natural reflectindo o seu tamanho). Assim, o facto de só podermos retirar e colocar um elemento de cada vez pelo topo da torre é reflectido pelo conjunto de operações disponíveis para manipular as pilhas: push serve para colocar um elemento no topo e pop serve para retirar o elemento no topo.
Assumindo que, de algum modo, somos capazes de definir em F o tipo stack, o programa seguinte modela a tarefa com que se debatem os monges de Hanoi:
n = 64 call setup(n) call move(n,1,2,3) em que subroutine setup(n) integer, intent(in) :: n integer :: i t(1) = new() t(2) = new() t(3) = new() k = 0 do i = n,1,-1 call push(i,t(1)) end do call display()
end subroutine setup
recursive subroutine move(m,s,d,w) integer, intent(in) :: m,s,d,w if (m == 1) then call push(top(t(s)),t(d)) call pop(t(s)) k = k + 1 call display() else call move(m-1,s,w,d) call push(top(t(s)),t(d)) call pop(t(s)) k = k + 1 call display() call move(m-1,w,d,s) end if
end subroutine move
O programa anterior utiliza duas variáveis t e k. A variável t é um vector de tamanho 3 e de tipo stack, correspondendo às 3 torres de Hanoi. A variável k é utilizada para contar o número de movimentos efectuados. Fazendo correr com n=4 obtemos (a subrotina display é descrita a seguir). | 4 3 2 1 | | --- move 0 | 4 3 2 | | 1 --- move 1 | 4 3 | 2
| 1 --- move 2 | 4 3 | 2 1 | --- move 3 | 4 | 2 1 | 3 --- move 4 | 4 1 | 2 | 3 --- move 5 | 4 1 | | 3 2 --- move 6 | 4 | | 3 2 1 --- move 7 | | 4 | 3 2 1 --- move 8 | | 4 1 | 3 2 --- move 9 | 2 | 4 1 | 3 --- move 10 | 2 1 | 4 | 3 --- move 11 | 2 1 | 4 3 | --- move 12 | 2 | 4 3 | 1 --- move 13 | | 4 3 2 | 1 --- move 14 | | 4 3 2 1 | --- move 15
Vale a pena explicar como funciona a subrotina move que é o elemento crucial do programa. Em geral, o problema de mover n discos de um poste s para um poste d resume-se a mover n-1 discos do poste de s para o outro poste, w, ficando apenas um disco em s. Este disco pode ser então movido para o poste d usando as operações sobre pilhas definidas. Finalmente, há que mover os n-1 discos que estão em w para d. Para mover n-1 discos, há que primeiro mover n-2, e assim sucessivamente, até se chegar a um ponto em que apenas falta mover um disco.
Este problema das torres de Hanoi foi resolvido seguindo o método de programação por camadas
baseadas em objectos, o qual consiste em:
1. Identificar a camada dos tipos de objectos relevantes. [Neste caso precisamos de uma camada que disponibilize objectos de tipo pilha de inteiros.]
2. Desenvolver o programa abstracto supondo que a camada existe. [Neste caso escrevemos o programa acima auxiliado pelas subrotinas setup e move.]
3. Implementar a camada de modo eficiente sobre os tipos primitivos da linguagem F. [Veremos adiante como tal pode ser feito usando a construção module da linguagem F]
4. Integrar o programa abstracto e a implementação da camada no programa operacional. [Veremos adiante como utilizar uma camada já definida por um módulo.]
Note-se que este método pode ser aplicado iterativamente, conduzindo a uma sequência de camadas em que a mais inferior se implementa directamente sobre os tipos de objectos primitivos da linguagem F e em que cada camada se implementa sobre a que lhe está imediatamente abaixo. Nos capítulos seguintes (por exemplo no dedicado à simulação estocástica) veremos exemplos de aplicação iterativa do método. Neste primeiro exemplo do problema das torres de Hanoi, como já vimos, é necessária apenas uma camada para facultar o tipo das pilhas de inteiros.
As vantagens deste método são inegáveis, incluindo:
Separação entre os aspectos procedimentais e a estruturação dos objectos de trabalho, que nos permite concentrar em cada um dos subproblemas separadamente ou que sejam programadores diferentes encarregados do programa abstracto e da implementação da camada. [No caso vertente, é realmente vantajoso tentar escrever a subrotina move sem termos de nos preocupar ao mesmo tempo com a manipulação das torres (pilhas).]
Independência da implementação: o programa abstracto é independente dos detalhes de implementação da camada, o que permite ulteriormente alterar a implementação da camada (por exemplo, para a optimizar) sem ser necessário modificar o programa. Para tal, é importante que a linguagem de programação utilizada permita esconder os detalhes da implementação de uma camada. Como veremos, a construção module da linguagem F permite declarar como public / private os elementos da camada que pretendemos que sejam visíveis / invisíveis do exterior. Assim, o programador responsável pelo programa abstracto nem inadvertidamente poderá utilizar os detalhes irrelevantes da implemantação. [No caso vertente, tudo o que foi necessário saber sobre a camada das pilhas foi o conjunto das funções (new, top, emptyQ) e subrotinas (push, pop) disponibilizadas bem como dos tipos definidos (no caso apenas um, stack).] Reutilização de código: uma camada pode ser utilizada em muitos outros programas pois os
objectos tendem a ser muito mais universais que as soluções procedimentais. [No caso vertente, é de facto assim: as pilhas são úteis em inúmeros problemas, mas a subrotina move só é interessante para resolver o problema das torres de Hanoi.]
Voltando ao problema das torres de Hanoi, já vimos antes como foram aplicados os passos 1 e 2 do método de programação modular. O passo seguinte (passo 3) consiste em definir a implementação da camada que disponibiliza o tipo das pilhas de inteiros:
module mbstacks
public :: new, push, pop, top, emptyQ, fullQ, erase, show type, public :: stack
private
integer :: index
integer, dimension(1:64) :: array end type stack
contains
function new() result(r) type(stack) :: r
r%index=0
end function new subroutine push(x,s) integer, intent(in) :: x type(stack), intent(inout) :: s if (s%index<64) then s%index=s%index+1 s%array(s%index)=x end if
end subroutine push subroutine pop(s)
type(stack), intent(inout) :: s if (s%index>0) then
s%index=s%index-1 end if
end subroutine pop
function top(s) result(y) type(stack), intent(in) :: s integer :: y
if (s%index>0) then y=s%array(s%index) end if
end function top
function emptyQ(s) result(b) type(stack), intent(in) :: s logical :: b if (s%index==0) then b=.true. else b=.false. end if
end function emptyQ
function fullQ(s) result(b) type(stack), intent(in) :: s logical :: b if (s%index==64) then b=.true. else b=.false. end if
end function fullQ subroutine erase(s)
type(stack), intent(inout) :: s s%index=0
end subroutine erase subroutine show(s)
print *, "|", s%array(1:s%index) end subroutine show
end module mbstacks
Vale a pena comentar as diversas componentes deste módulo. Comecemos pela definição do tipo stack:
type, public :: stack private
integer :: index
integer, dimension(1:64) :: array end type stack
Esta construção permite definir um novo tipo: uma estrutura com duas componentes, um número inteiro, referenciado por index, e um vector de números inteiros de comprimento 64, referenciado por array. A figura seguinte ilustra um objecto deste tipo (uma pilha contendo, por ordem de sobreposição, os elementos 11, 22, 33, 44):
11 22 33 44 ... array 4 index
Os objectos deste tipo vão ser utilizados para representar pilhas de inteiros (de profundidade máxima 64), em que a componente array serve para armazenar os elementos da pilha, a componente index guarda a posição onde se encontra o elemento topo da pilha. A utilização do comando private na definição do tipo impede que a estrutura deste seja conhecida fora do módulo onde este foi definido.
A declaração de variáveis de um tipo definido pelo utilizador é semelhante à definição de variáveis de um tipo primitivo. Considere-se a declaração de uma variável t de tipo stack:
type(stack) :: t
A manipulação de objectos deste tipo é feita da seguinte forma: t%index permite aceder à componente index, que é de tipo integer; t%array permite aceder à componente array, que é um vector de inteiros.
Consideremos agora as funções e subrotinas para manipular objectos deste tipo. A função new devolve a pilha vazia, ou seja, cria um novo objecto de tipo pilha (através da declaração da variável r), colocando a componente index a zero (a pilha ainda não tem nenhum elemento):
function new() result(r) type(stack) :: r
r%index = 0 end function new
A subrotina push recebe dois argumentos, x inteiro e s stack, e sobrepõe o elemento x na pilha s, ou seja, há que incrementar o index de uma posição, o novo topo, e guardar x nessa posição do array: subroutine push(x,s)
integer, intent(in) :: x
type(stack), intent(inout) :: s if (s%index < 64) then
s%index = s%index + 1 s%array(s%index) = x end if
end subroutine push
Se o sistema estiver num estado em que o conteúdo da variável s é:
11 22 33 44 ... array 4 index
após a execução push(55,s) o conteúdo da variável s será:
11 22 33 44 55 ... array 5 index
A subrotina pop recebe como argumento uma pilha s, à qual é retirado o elemento no topo. Para tal, basta decrementar a posição do topo, guardada em index:
subroutine pop(s)
type(stack), intent(inout) :: s if (s%index > 0) then
s%index = s%index - 1 end if
end subroutine pop
Se estivermos num estado em que o conteúdo da variável s é:
11 22 33 44 55 ... array 5 index
após a execução pop(s) o conteúdo da variável s será:
11 22 33 44 55 ... array 4 index
Repare-se que o conteúdo de array da posição 4 em diante é irrelevante. São posições que estão acima do topo (podemos considerar que são lugares livres, disponíveis para armazenar informação no futuro). Por isso mesmo a subrotina pop não precisa de apagar o número 55. Basta decrementar o valor de index. Se, em seguida, for sobreposto um novo elemento em s, através da subrotina push, esse valor será escrito por cima do 55.
A função top devolve o elemento topo de uma pilha s (caso exista). O topo da pilha é o valor que está na posição de array guardada em index:
function top(s) result(y) type(stack), intent(in) :: s integer :: y
if (s%index > 0) then y = s%array(s%index) end if
end function top
Se estivermos num estado em que o conteúdo da variável s é:
11 22 33 44 55 ... array 4 index
então o topo de s será o valor guardado na posição 4 (index) do array, ou seja, 44.
A função emptyQ recebe como argumento uma pilha s testa se esta se encontra vazia, ou seja, testa se index é 0.
function emptyQ(s) result(b) type(stack), intent(in) :: s logical :: b if (s%index == 0) then b = .true. else b = .false. end if
end function emptyQ
O módulo apresentado inclui outras funções úteis para manipular pilhas, cuja análise se deixa como exercício.
Finalmente estamos em condições de ligar o programa abstracto à camada que acabámos de definir. O resultado deste passo (o passo 4) do método resulta neste caso no programa operacional seguinte:
program bhanoi use mbstacks type(stack), dimension(1:3) :: t integer :: n,k n = 4 call setup(n) call move(n,1,2,3) contains subroutine display() call show(t(1)) call show(t(2))
call show(t(3))
print *, repeat("-",2*n+1)," move ", k end subroutine display
subroutine setup(n) integer, intent(in) :: n integer :: i t(1) = new() t(2) = new() t(3) = new() k = 0 do i = n,1,-1 call push(i,t(1)) end do call display()
end subroutine setup
recursive subroutine move(m,s,d,w) integer, intent(in) :: m,s,d,w if (m == 1) then call push(top(t(s)),t(d)) call pop(t(s)) k = k + 1 call display() else call move(m-1,s,w,d) call push(top(t(s)),t(d)) call pop(t(s)) k = k + 1 call display() call move(m-1,w,d,s) end if
end subroutine move end program bhanoi
Este programa utiliza três variáveis de trabalho: um vector t de tamanho 3, de tipo pilha (correspondente aos três postes), uma variável k, para contar o número de movimentos, e uma variável n, para fixar o número de discos. A camada das pilhas é invocada pelo comando:
use mbstacks
que viabiliza a utilização das operações sobre pilhas no contexto do programa bhanoi. Assim, de facto, temos o programa bhanoi definido sobre a camada mbstacks:
mfstacks Linguagem F
Note-se que a implementação apresentada acima das pilhas sobre vectores é estática no sentido em que o tamanho máximo de cada pilha está fixado (em 64). Se o módulo for invocado com o número de discos (n) maior do que 64, a execução termina com um erro (exercício: experimente!). Embora tal não seja necessário para resolver o problema clássico das torres de Hanoi (em que n=64), seria bom que pudéssemos definir uma implementação mais flexível das pilhas que permitisse trabalhar com pilhas de tamanho arbitrário não conhecido no momento da compilação do programa, o que, aliás, é essencial na maioria dos problemas que envolvem pilhas (como será ilustrado mais adiante).
De facto, na linguagem F é possível definir objectos dinâmicos cujo tamanho não fica fixado no momento da compilação, podendo ser fixado só no momento da criação ou até mesmo alterado durante a vida do objecto, como se explica de seguida.