Você está na página 1de 96

Introdução à Programação na linguagem F

Jaime Ramos, Amílcar Sernadas e Paulo Mateus

DMIST, Setembro de 2005


Capítulo 1 Introdução à linguagem F
Objectivos

Introdução à linguagem F. Edição e compilação de programas. Tipos primitivos e expressões. Declaração


de variáveis. Atribuição. Composição sequencial, iterativa e alternativa de comandos. Definição de
funções e subrotinas. Efeitos colaterais. Programação recursiva.

Conceitos básicos

A linguagem F é um fragmento cuidadosamente escolhido da linguagem de programação Fortran 95.


Considere-se o seguinte programa F, para somar os números inteiros entre 1 e 50.

program exemplo1

integer :: i, r

r=0
do i=1,50
r=r+i
end do
print *,"Resultado:",r
end program exemplo1

Antes de proceder à análise do programa, convém perceber como é que se edita, compila e executa um
programa em F. A edição do programa é feita do modo usual: recorrendo a um editor de texto (por
exemplo o Notepad), escreve-se o código do programa num ficheiro com extensão f95. Neste caso, deu-
se ao ficheiro o nome exemplo1.f95 (embora não seja obrigatório, é usual e conveniente dar ao
ficheiro o mesmo nome do programa).

Em seguida, é necessário compilar o programa. Para tal, é necessário ter instalado no computador em que
se está a trabalhar um compilador para a linguagem F (que pode ser obtido em www.fortran.com/F).
No caso da compilação ser bem sucedida (não ocorrerem erros), o resultado é um ficheiro executável
(extensão exe).

Analisemos este programa. Após a declaração de início do programa (program exemplo1) surge a
declaração das variáveis utilizadas. Nesta declaração, deverão ser incluídas todas as variáveis que sejam
utilizadas pelo programa bem como o tipo de objecto que cada uma dessas variáveis irá guardar durante a
execução do programa. Neste caso, são declaradas duas variáveis i e r, para guardar números inteiros,
através da declaração:

integer :: i, r

Com esta declaração, as variáveis i e r apenas poderão ser utilizadas para guardar números inteiros.
Qualquer tentativa de lhes atribuir um valor que não seja um número inteiro (por exemplo um número
real) resultará num erro do programa.

Segue-se o corpo do programa constituído por uma composição sequencial de três instruções: a atribuição
r=0, a instrução composta do ... end do e a instrução de escrita print *,"Resultado:",r. A
composição sequencial de comandos é feita através da mudança de linha, ou seja, em F apenas é
permitido uma instrução atómica por linha (por exemplo, uma atribuição ou uma instrução de escrita). A
estrutura das instruções compostas será discutida à medida que estas forem sendo apresentadas.

A forma genérica da instrução iterativa é a seguinte:

do guarda
corpo
end do

2
em que guarda é uma expressão da forma

variável = expressão1, expressão2

e corpo é uma instrução.

A variável recebe como valor inicial o valor da expressão1 e o corpo do ciclo é executado. Em
seguida o valor da variável é incrementado de uma unidade e, caso o valor da expressão2 não
tenha sido excedido, o corpo do ciclo volta a ser executado. Este processo repete-se até que o valor da
expressão2 seja excedido. Mais à frente, serão apresentadas outras formas de controlar a execução de
um ciclo.

No caso do exemplo, a instrução

do i=1,50
r=r+i
end do

repete a atribuição r=r+i, começando a variável i com o valor 1 (expressão1) até se atingir o valor
50 (expressão2). De cada vez que a atribuição é executada o valor de i é incrementado.

A última instrução do programa é uma instrução de escrita. Em F, a instrução print pode ser
formatada para apresentar os resultados de diferentes formas. Por agora, apenas nos preocupamos em
escrever informação no ecrã de uma forma não formatada, através da opção *. Mais tarde analisaremos
algumas formas de formatar os resultados.

O programa termina com end program exemplo1.

Existem também em F instruções de leitura. O programa seguinte, guardado no ficheiro


exemplo2.f95, utiliza a instrução read para ler do terminal dois números inteiros para as variáveis x
e y. Em seguida soma os números inteiros entre x e y:

program exemplo2

integer :: x, y, i, r

print *,"Valor inicial:"


read *,x
print *,"Valor final:"
read *,y
r=0
do i=x,y
r=r+i
end do
print *,"Resultado:",r
end program exemplo2

Então, após compilar o programa podemos executá-lo. O resultado é o seguinte:

Valor inicial:
1
Valor final
50
Resultado: 1275

Os valores 1 e 50 foram introduzidos pelo utilizador, a partir do terminal, e lidos para as variáveis x e y,
respectivamente, através da instrução read.

Tipos primitivos e expressões

3
Existem 5 tipos de dados primitivos disponíveis em F: integer, real, complex, logical e
character. Mais tarde, serão apresentadas construções que permitem definir tipos mais complexos em
F.

Tipo integer: os objectos deste tipo correspondem aos números inteiros. Por exemplo, 45, 0, -100
são objectos de tipo integer. Há ainda a possibilidade de escolher a representação destes números
(long ou short); essa discussão é adiada para um capítulo subsequente.

Tipo real: os objectos deste tipo correspondem aos números reais. Por exemplo, 3.1416, 0.13, 3.0
são objectos de tipo real. Tal como no tipo integer, é possível escolher a representação destes
números.

Tipo complex: os objectos deste tipo correspondem aos números complexos (da forma a+bi, em que
tanto a como b são números reais). Por exemplo, (1.5, 1.3) é um objecto de tipo complex que
corresponde ao número complexo 1.5 + 1.3i.

Tipo logical: os objectos deste tipo correspondem aos valores de verdade. As constantes são .true.
e .false. correspondendo a verdadeiro e a falso, respectivamente.

Tipo character: os objectos deste tipo correspondem a cadeias de caracteres (delimitadas por aspas).
Por exemplo, "Joao" e "ola" são objectos de de tipo character.

Estão disponíveis operações aritméticas definidas sobre cada os tipos números, como por exemplo,
operações usuais + (adição), - (subtracção), * (multiplicação), / (divisão). Estas operações, quando
aplicadas a dois objectos do mesmo tipo, resultam num objecto desse tipo, ou seja, adicionar dois
números inteiros resulta num número inteiro, multiplicar dois números reais resulta num número real, etc.
Nomeadamente, a divisão de dois números inteiros resulta num número inteiro. Por exemplo, o resultado
de avaliar a expressão 15/2 em F é o número inteiro 7. Obviamente, estas operações numéricas podem
ser aplicadas a objectos de diferentes tipos. Nesse caso, prevalece o tipo mais geral. Por exemplo, somar
um número inteiro com um número real resulta num número real, enquanto multiplicar um número real
por um número complexo resulta num número complexo. Para além destas, existem outras operações
aritméticas definidas em F, cuja listagem exaustiva se omite. Deixa-se como exercício interpretar
expressão n-(n/i)*i em que n e i são variáveis de tipo integer.

Para além das operações aritméticas, estão disponíveis também as habituais operações relacionais. As
mais frequentes são: == (igualdade), /= (diferente), < (menor), <= (menor ou igual), > (maior), >=
(maior ou igual). O resultado de uma operação relacional é um objecto de tipo logical. Por exemplo, a
expressão 2>=0 será avaliada em F para o valor .true., enquanto a expressão 0>3 será avaliada para
.false.. É preciso cuidado na comparação de números reais e complexos usando as operações == e
/=, devido a eventuais erros de aproximação.

Relativamente aos objectos de tipo logical, estão disponíveis as seguintes operações: .not., .and.,
.or., .eqv., and .neqv.. Estas operações podem ser aplicadas a objectos de tipo logical sendo o
resultado também um objecto de tipo logical. Por exemplo, a expressão 2>0 .and. 3>2 quando
avaliada em F resulta no valor .true..

Composição alternativa de instruções

Em F, a composição alternativa de instruções tem duas formas principais. A primeira manda executar
uma instrução caso a condição seja verdadeira e não faz nada se esta for falsa:

if (condição) then
instrução1
end if

A segunda forma permite mandar executar uma instrução no caso da condição ser falsa:

if (condição) then
instrução1

4
else
instrução2
end if

A condição tem que estar entre parênteses.

O exemplo seguinte, guardado no ficheiro exemplo3.f95, ilustra a utilização destas duas formas:

program exemplo3

integer :: n, i, d

print *,"Introduza um numero:"


read *,n
d=0
do i=2,n-1
if (n-(n/i)*i == 0) then
d=d+1
end if
end do
if (n>1 .and. d == 0) then
print *,"O numero e primo."
else
print *,"O numero nao e primo."
end if
end program exemplo3

Este programa determina se um dado número n, lido do terminal, é primo ou não. Para tal, contam-se os
números entre 2 e n-1 que dividem o número dado. O ciclo do programa percorre os números entre 2 e
n-1 e caso i seja um divisor de n (o que, como já vimos, corresponde à expressão aritmética n-
(n/i)*i ser igual a zero), a variável d, que conta o número de divisores, é incrementada. Caso
contrário, não se faz nada. No fim, observa-se o valor da variável d e se for igual a zero então é porque o
número é primo. Caso contrário o número tem pelo menos um divisor diferente dele e da unidade logo
não é primo. Considerem-se alguns exemplos de execução:

Introduza um numero:
3
O numero e primo.

Introduza um numero:
51
O numero nao e primo.

Introduza um numero:
1021
O numero e primo.

Deixa-se como exercício repetir alguns dos programas MATLAB apresentados anteriormente.

Composição iterativa revisitada

Até agora, a execução de um ciclo obriga a variável do ciclo a percorrer todos os valores entre o valor da
expressão1 e o valor da expressão2. Suponha-se que se pretende apenas analisar os números
pares. Por exemplo, pretende-se um programa que imprima no ecrã todos os números pares até 50.
Obviamente que com as instruções disponíveis é possível construir tal programa (como?). No entanto, tal
solução obriga a percorrer todos os números entre 1 e 50, quando nós apenas estamos interessados nos
números pares entre 2 e 50. A linguagem F disponibiliza um mecanismo para controlar o progresso da
variável do ciclo. A guarda do ciclo pode ter a forma

variável = expressão1, expressão2, expressão3

5
em que expressão3 especifica o incremento da variável. Por exemplo, no programa exemplo4
seguinte, a guarda i=2,50,2 especifica que a variável i toma o valor inicial 2 e é incrementada de 2
em 2 até se atingir 50.

program exemplo4

integer :: i

do i=2,50,2
print *,i
end do
end program exemplo4

Ao executar este programa o resultado obtido é a lista de todos os números pares escritos no ecrã:

2
4
6

O incremento também pode ser negativo, mas neste caso há que garantir que o valor inicial é maior do
que o valor final. Considere-se o programa seguinte, para calcular o factorial de um número.

program exemplo5

integer :: n, r, i

print *,"Introduza um número: "


read *,n
r=1
do i=n,1,-1
r=r*i
end do
print *,"O factorial e: ",r
end program exemplo5

Neste caso, a variável i toma o valor inicial n e é decrementada até se atingir o valor 1.

Existem ainda outras formas de controlar a execução de uma instrução iterativa que serão ilustradas mais
à frente.

Funções e subrotinas auxiliares

Ao desenvolver um programa, é muitas vezes necessário definir funções e subrotinas auxiliares. No


seguimento, apresentam-se alguns exemplos onde a definição de funções e subrotinas auxiliares se revela
útil. Mais à frente, estes conceitos voltarão a ser aplicados ao desenvolvimento de grandes programas,
segundo a metodologia da programação modular por camadas baseadas em objectos.

Suponha-se que se pretende desenvolver um programa para contar o número de números primos que
existem até um determinado número n, dado pelo utilizador. Embora seja possível desenvolver um
programa em F sem recorrer a procedimentos auxiliares, tal solução tem diversos inconvenientes, entre os
quais se destacam: o desenvolvimento do programa é mais complexo, a detecção de erros é mais difícil e
a leitura do programa mais complicada. Uma solução consiste em definir uma função auxiliar que testa se
um número é primo e utilizar essa função para construir o programa principal.

Uma função em F pode ser utilizada como qualquer função primitiva. Não pode ter efeitos colaterais e
devolve sempre um valor. Uma subrotina assemelha-se a uma função, mas não devolve valor, embora
alguns dos seus parâmentros possam ser utilizados para devolver valores ao programa que a invocou, e
pode também ter efeitos colaterais.

6
Suponha-se então que se dispõe de uma função prime para testar se um determinado número é primo.
Uma solução para o problema apresentado é o programa seguinte:

program exemplo6

integer :: n, i, r

print *,"Introduza um numero:"


read *,n
r=0
do i=1,n
if (prime(i)) then
r=r+1
end if
end do
print *,"Existem",r,"primos ate",n
end program exemplo6

Se tentarmos compilar o programa anterior obtemos um erro de compilação pois a função prime não é
conhecida. É necessário definir esta função e torná-la conhecida para o programa.

Comecemos pela definição da função. A estrutura de uma função em F é semelhante à de um programa:

function prime(n) result(b)


integer, intent(in) :: n
logical :: b
integer :: i, d

if (n<=1) then
b= .false.
else
d=0
do i=2,n-1
if (n-(n/i)*i == 0) then
d=d+1
end if
end do
if (d==0) then
b=.true.
else
b=.false.
end if
end if
end function prime

As principais diferenças de uma função para um programa são a utilização da instrução function em
vez da instrução program, e a necessidade de declarar os parâmetros e o resultado da função. Neste
caso, a função prime tem um parâmetro de entrada n de tipo inteiro e um resultado b de tipo logical.
A declaração intent(in) serve para indicar que o parâmetro apenas pode ser utilizado para passar
valores à função. Veremos a seguir que podem existir outros tipos de argumentos quando se definirem
subrotinas. Em tudo o resto a definição da função é semelhante a um programa.

Falta apenas disponibilizar esta função ao programa anterior. A solução mais simples consiste em declarar
esta função no fim do programa, após o corpo do programa e antes da instrução end program, usando
a instrução contains. O ficheiro exemplo6.f95 contem a versão completa do programa:

program exemplo6

integer :: n, i, r

7
print *,"Introduza um numero:"
read *,n
r=0
do i=1,n
if (prime(i)) then
r=r+1
end if
end do
print *,"Existem",r,"primos ate",n

contains

function prime(n) result(b)


integer, intent(in) :: n
logical :: b
integer :: i, d

if (n<=1) then
b= .false.
else
d=0
do i=2,n-1
if (n-(n/i)*i == 0) then
d=d+1
end if
end do
if (d==0) then
b=.true.
else
b=.false.
end if
end if
end function prime
end program exemplo6

Após compilarmos o programa anterior podemos testá-lo. Seguem-se alguns exemplos:

Introduza um numero:
4
Existem 2 primos ate 4

Introduza um numero:
10
Existem 4 primos ate 10

Uma subrotina assemelha-se a uma função mas existem algumas diferenças. As principais diferenças
residem no facto de uma subrotina não devolver valor e poder provocar efeitos colaterais. No programa
anterior, em vez de uma função, podia ter sido definida uma subrotina prime, com dois parâmetros, um
parâmetro de entrada n e um parâmetro de saída b, em que se devolve o resultado.

subroutine prime(n,b)
integer, intent(in) :: n
logical, intent(out) :: b
integer :: i, d

if (n<=1) then
b= .false.
else
d=0
do i=2,n-1
if (n-(n/i)*i == 0) then

8
d=d+1
end if
end do
if (d==0) then
b=.true.
else
b=.false.
end if
end if
end subroutine prime

A declaração intent(out) define o parâmetro b como sendo de saída. A chamada de uma subrotina
também é diferente da chamada de uma função. Neste caso, a chamada da subrotina anterior é feita
através da instrução call. O programa exemplo7 ilustra a utilização desta subrotina, para contar os
números primos até um determinado número.

program exemplo7

integer :: n, i, r
logical :: b

print *,"Introduza um numero:"


read *,n
r=0
do i=1,n
call prime(i,b)
if (b) then
r=r+1
end if
end do
print *,"Existem",r,"primos ate",n

contains

subroutine prime(n,r)
integer, intent(in) :: n
logical, intent(out) :: r
integer :: i, d

if (n<=1) then
r= .false.
else
d=0
do i=2,n-1
if (n-(n/i)*i == 0) then
d=d+1
end if
end do
if (d==0) then
r=.true.
else
r=.false.
end if
end if
end subroutine prime
end program exemplo7

Ao chamar a subrotina prime, o resultado da chamada fica na variável b do programa, e é esta variável
b que é usada para verificar se a variável r tem ou não que ser incrementada.

9
Do que foi visto, podemos concluir que uma função pode ser vista como um valor, isto é, pode ser
utilizada numa expressão, enquanto uma subrotina pode ser encarada como uma nova instrução, que, ao
ser executada, pode provocar uma mudança no estado do sistema. No caso exemplo anterior, cada
chamada à subrotina prime provoca (eventualmente) uma alteração da variável b.

Apresentam-se em seguida alguns exemplos complementares de definição de funções e subrotinas


auxiliares.

Recorde-se o problema de calcular o factorial de um número. A primeira solução passa por definir uma
função auxiliar fact com um parâmetro que devolve o factorial desse parâmetro

program exemplo8

integer :: n

print *,"Introduza um numero:"


read *,n
print *,"Factorial:", fact(n)

contains

function fact(n) result(y)


integer, intent(in) :: n
integer :: y, i

y=1
do i=2,n
y=y*i
end do

end function fact


end program exemplo8

Eis alguns exemplos de execução:

Introduza um numero:
3
Factorial: 6

Introduza um numero:
5
Factorial: 120

Este problema poderia também ter sido resolvido através da definição de uma subrotina. Neste caso,
temos duas hipóteses: utilizar um parâmetro como entrada e outro parâmetro como saída, ou utilizar
apenas um parâmetro para entrada e saída dos dados. A primeira solução não apresenta nada de novo e
deixa-se como exercício. A segunda tem a novidade de podermos ter parâmetros de entrada e saída
simultânea.

program exemplo9

integer :: n

print *,"Introduza um numero:"


read *,n
call fact(n)
print *,"Factorial:", n

contains

subroutine fact(n)

10
integer, intent(inout) :: n
integer :: y, i

y=n
do i=2,y-1
n=n*i
end do
end subroutine fact
end program exemplo9

Observe-se que na definição da subrotina foi utilizada a declaração intent(inout) para o parâmetro
n. Isto significa que não só podemos passar valores à subrotina através do parâmetro, como este poderá
ser alterado durante a execução da subrotina e essas alterações reflectir-se-ão para fora. Com efeito, a
chamada a esta subrotina a partir do programa principal reflecte isso mesmo. Antes da chamada, n guarda
o número de que pretendemos calcular o factorial. Após a chamada da subrotina, n guarda o valor do
factorial.

Introduza um numero:
3
Factorial: 6

Introduza um numero:
5
Factorial: 120

Deixa-se como exercício definir a subrotina anterior usando apenas o parâmetro n e a variável i.

Efeitos colaterais

Pode acontecer que uma subrotina modifique o valor de uma variável global (uma variável do programa
que a invocou). Como já foi dito atrás, a este tipo de efeitos dá-se o nome de efeito colateral. Considere-
se o problema de calcular o valor do factorial de um número. Uma solução, diferente das apresentadas
anteriormente, consiste em definir uma subrotina sem parâmetros, mas que manipule directamente o valor
da variável.

program exemplo10

integer :: n

print *,"Introduza um numero:"


read *,n
call fact()
print *,"Factorial:", n

contains

subroutine fact()
integer :: y, i

y=n
do i=2,y-1
n=n*i
end do
end subroutine fact
end program exemplo10

Repare-se que a subrotina é em tudo semelhante à do exemplo9. Do ponto de vista do programa


principal, a variável n foi alterada sem indicação explícita. No entanto, o programa funciona como
esperado:

11
Introduza um numero:
3
Factorial: 6

Introduza um numero:
5
Factorial: 120

Como já foi dito atrás, os efeitos colaterais apenas podem ser provocados por uma subrotina e nunca por
uma função.

Programação recursiva

Consiste em definir funções à custa de si mesmas. A definição de uma função ou subrotina recursiva em F
é semelhante à definição de uma função ou subrotina. O facto de a função ou subrotina ser recursiva tem
que ser indicado explicitamente através da instrução recursive.

Recorde-se a expressão recursiva para definir o factorial de um número natural:

 1 se n  0
n! = 
n.(n  1)! se n  0

A função F seguinte calcula recursivamente o factorial de um número natural:

recursive function factR(n) result(r)


integer, intent(in) :: n
integer :: r

if (n==0) then
r = 1
else
r = n*factR(n-1)
endif
end function factR

O programa seguinte utiliza esta função para calcular o factorial de um número fornecido pelo utilizador:

program factorialR

integer :: n

print *,"Introduza um numero:"


read *,n
print *,"Factorial:",factR(n)

contains

recursive function factR(n) result(r)


integer, intent(in) :: n
integer :: r

if (n==0) then
r = 1
else
r = n*factR(n-1)
endif
end function factR
end program factorialR

12
Considerem-se alguns exemplos de utilização:

Introduza um numero:
4
Factorial: 24

Introduza um numero:
1
Factorial: 1

Deixa-se como exercício proteger a função contra argumentos indesejados. nomeadamente, contra
números negativos.

Considere-se a sucessão Fn, n≥0, dos números de Fibonacci definida por

 0 se n  0

Fn=  1 se n  1
Fn  2  Fn  1 se n  1

A correspondente definição em F é a seguinte:

recursive function fibR(n) result(r)


integer, intent(in) :: n
integer :: r

if (n==0) then
r = 0
else if (n==1) then
r = 1
else
r = fibR(n-2)+fibR(n-1)
endif
end function fibR

Podemos testar esta função através do programa:

program fib

integer :: n

print *,"Introduza um numero:"


read *,n
print *,"Numero de Fibonacci:",fibR(n)

contains

recursive function fibR(n) result(r)


integer, intent(in) :: n
integer :: r

if (n==0) then
r = 0
else if (n==1) then
r = 1
else
r = fibR(n-2)+fibR(n-1)
endif
end function fibR
end program fib

13
Apresentam-se em seguida alguns exemplos de utilização:

Introduza um número:
2
Numero de Fibonacci: 1

Introduza um número:
6
Numero de Fibonacci: 8

Capítulo 2 Complementos de programação em F


Objectivos

Vectores e matrizes. Programação imperativa sobre vectores e matrizes. Programação recursiva sobre
vectores.

Vectores

Para declarar uma variável vectorial, é necessário indicar qual o tamanho do vector em causa e qual o tipo
de objectos desse vector. Na instrução seguinte declara-se uma variável v para guardar vectores de
números inteiros com tamanho 5:

integer, dimension(5) :: v

Alternativamente, podia ter sido utilizada a declaração seguinte, em que se delimitam o limite inferior e
superior do vector

integer, dimension(1:5) :: v

Para aceder aos elementos do vector, utiliza-se a expressão v(i), que se refere ao elemento na posição i
do vector guardado em v.

Pretende-se definir uma função que recebe como argumento um vector de tamanho 5 e retorna a soma dos
seus elementos. Como primeira solução, considere-se a seguinte função:

function somaVec1(v) result(s)


integer, dimension(5), intent(in) :: v
integer :: s, i

s=0
do i=1,5
s=s+v(i)
end do
end function somaVec1

Tal como pretendido, esta função recebe como argumento um vector de tamanho 5, soma as suas
componentes uma a uma recorrendo a um ciclo do e devolve essa soma. O programa seguinte ilustra a
utilização desta função: lê do terminal cinco números inteiros, guarda-os num vector, e calcula a soma
dos seus elementos recorrendo à função anterior.

program exemplo11

integer, dimension(5) :: v
integer :: i

14
do i=1,5
print *,"Introduza um numero:"
read *,v(i)
end do
print *,"O resultado e:",somaVec1(v)

contains

function somaVec1(v) result(s)


integer, dimension(5), intent(in) :: v
integer :: s, i
s=0
do i=1,5
s=s+v(i)
end do
end function somaVec1
end program exemplo11

Considere-se a seguinte execução:

Introduza um numero:
1
Introduza um numero:
2
Introduza um numero:
3
Introduza um numero:
4
Introduza um numero:
5
O resultado e: 15

A pergunta seguinte surge naturalmente: e se pretendermos somar os elementos de vectores com outros
tamanhos? Podem surgir duas situações: ou o vector tem tamanho superior a 5 e nesse caso a função
apenas soma os cinco primeiros elementos, ou o vector tem tamanho inferior a 5 e nesse caso ocorre um
erro de compilação.

No entanto a função anterior pode ser generalizada para vectores de qualquer tamanho. O problema da
declaração do parâmetro resolve-se recorrendo à declaração:

integer, dimension(:), intent(in) :: v

Neste caso está-se a declarar um parâmetro que consiste um vector de tamanho arbitrário. O limite do
ciclo resolve-se recorrendo à função size. A função seguinte soma os elementos de um vector de
números inteiros de dimensão arbitrária:

function somaVec(v) result(s)


integer, dimension(:), intent(in) :: v
integer :: s, I

s=0
do i=1,size(v)
s=s+v(i)
end do
end function somaVec

O programa seguinte lê seis números inteiros do terminal, guarda-os num vector e, recorrendo à função
anterior, soma-os e apresenta o resultado:

program exemplo12

15
integer, dimension(6) :: v
integer :: i

do i=1,6
print *,"Introduza um numero:"
read *,v(i)
end do
print *,"O resultado e:",somaVec(v)

contains

function somaVec(v) result(s)


integer, dimension(:), intent(in) :: v
integer :: s, i
s=0
do i=1,size(v)
s=s+v(i)
end do
end function somaVec
end program exemplo12

Considere-se a seguinte execução:

Introduza um numero:
1
Introduza um numero:
2
Introduza um numero:
3
Introduza um numero:
4
Introduza um numero:
5
Introduza um numero:
6
O resultado e: 21

No próximo exemplo, define-se uma função para contar o número de números primos que ocorrem num
vector de números inteiros, recorrendo à função prime definida no capítulo anterior. A função contaP
recebe como argumento um vector de números inteiros e devolve o número de números primos que
ocorrem nesse vector. O programa seguinte começa por construir um vector com dez números inteiros
gerados aleatoriamente (entre 1 e 100) recorrendo à subrotina random, que gera números aleatórios entre
0 e 1. Em seguida chama a função contaP para contar quantos desses números gerados foram números
primos.

program contaprimos

integer, dimension(10) :: v
integer :: i
real :: x

do i=1,10
call random_number(x)
v(i)=int(x*100)+1
end do
print *,"Numero de primos:",contaP(v)

contains

function prime(n) result(b)


integer, intent(in) :: n

16
logical :: b
integer :: i, d

if (n<=1) then
b= .false.
else
d=0
do i=2,n-1
if (n-(n/i)*i == 0) then
d=d+1
end if
end do
if (d==0) then
b=.true.
else
b=.false.
end if
end if
end function prime

function contaP(v) result(s)


integer, dimension(:), intent(in) :: v
integer :: s, i
s=0
do i=1,size(v)
if (prime(v(i))) then
s=s+1
end if
end do
end function contaP
end program contaprimos

Considerem-se os seguintes exemplos de execução:

Numero de primos: 1

Numero de primos: 4

Considere-se agora o problema de calcular o produto interno de dois vectores. A função seguinte calcula
o produto interno de dois vectores, desde que eles sejam do mesmo tamanho. No caso de vectores com
tamanhos diferentes, o resultado é uma mensagem de erro no ecrã. O valor devolvido neste caso é
arbitrário.

function prodInterno(v1,v2) result(x)


real, dimension(:), intent(in) :: v1,v2
real :: x
integer :: i

if (size(v1)/=size(v2)) then
print *,"Erro! Os vectores nao sao do mesmo tamanho!"
else
x=0
do i=1,size(v1)
x=x+(v1(i)*v2(i))
end do
end if
end function prodInterno

Deixa-se como exercício testar esta função.

17
Considere-se o problema de calcular o produto de um vector por um escalar. A primeira solução consiste
numa função que recebe como argumentos o vector v e o escalar x e devolve o vector u resultante do
produto do vector pelo escalar, que deverá ser do mesmo tamanho do vector argumento. Como é que se
declara esse vector? Através da declaração:

integer, dimension(size(v)) :: u

A este tipo de declaração, em que o tamanho de um vector é fixado a partir do tamanho de outros
argumentos, dá-se o nome de vector automático.
Uma solução para a função prodEscalar é a seguinte:

function prodEscalar(v,x) result(u)


integer, dimension(:), intent(in) :: v
integer, intent(in) :: x
integer, dimension(size(v)) :: u
integer :: i

do i=1,size(v)
u(i)=v(i)*x
end do
end function prodEscalar

Apresenta-se em seguida um programa para testar esta função. Este programa atribui à variável v o vector
(1,2,3,4,5); repare-se na atribuição simultânea de um vector a uma variável.

program prodEscalar1

integer, dimension(5) :: v

v(1:5)=(/ 1,2,3,4,5 /)
print *,prodEscalar(v,2)

contains

function prodEscalar(v,x) result(u)


integer, dimension(:), intent(in) :: v
integer, intent(in) :: x
integer, dimension(size(v)) :: u
integer :: i

do i=1,size(v)
u(i)=v(i)*x
end do
end function prodEscalar
end program prodEscalar1

O resultado de execução deste programa é:

2 4 6 8 10

Alternativamente, podíamos ter optado por definir uma subrotina. Deixa-se como exercício definir uma
subrotina com três parâmetros, dois de entrada, o vector e o número, e um de saída, o vector resultado.
Alternativamente, pode definir-se uma subrotina que altera directamente o vector, evitando assim a
utilização de dois vectores. Para tal, declara-se o vector como parâmetro de entrada e saída (inout).

subroutine prodEscalar(v,x)
integer, dimension(:), intent(inout) :: v
integer, intent(in) :: x
integer :: i

do i=1,size(v)

18
v(i)=v(i)*x
end do
end subroutine prodEscalar

O programa anterior pode ser ligeiramente alterado para testar esta subrotina:

program prodEscalar2

integer, dimension(5) :: v

v(1:5)=(/ 1,2,3,4,5 /)
call prodEscalar(v,2)
print *,v

contains

subroutine prodEscalar(v,x)
integer, dimension(:), intent(inout) :: v
integer, intent(in) :: x
integer :: i

do i=1,size(v)
v(i)=v(i)*x
end do
end subroutine prodEscalar
end program prodEscalar2

O resultado de execução é o mesmo e omite-se.

Finalmente, apresenta-se uma subrotina para ordenar vectores de números inteiros (in situ), baseada no
método da selecção. Este método consiste em procurar o elemento mínimo e colocá-lo na primeira
posição o vector, em seguida procurar o menor elemento do vector de entre os restantes e colocá-lo na
segunda posição, e assim sucessivamente.

program selsort

integer, dimension(5) :: v
integer :: i

do i=1,5
print *,"Introduza um numero:"
read *,v(i)
end do
call selectSort(v)
print *,v

contains

subroutine swap(v,m,k)
integer, dimension(:), intent(inout) :: v
integer, intent(in) :: m,k
integer :: x

x=v(m)
v(m)=v(k)
v(k)=x
end subroutine swap

subroutine selectSort(v)
integer, dimension(:), intent(inout) :: v
integer :: m, k

19
if (size(v)>1) then
do m=1,size(v)-1
do k=m+1,size(v)
if(v(m)>v(k)) then
call swap(v,m,k)
end if
end do
end do
end if
end subroutine selectSort
end program selsort

Este programa, para além da subrotina selectSort, utiliza ainda a subrotina swap que permite trocar
os elementos nas posições m e k do vector v.

Ao executar este programa, obtém-se o seguinte resultado:

Introduza um numero:
9
Introduza um numero:
4
Introduza um numero:
7
Introduza um numero:
3
Introduza um numero:
10
Vector ordenado: 3 4 7 9 10

Matrizes

A declaração de matrizes é semelhante à dos vectores, mas em vez de apenas uma dimensão, há que
considerar duas (ou mais) dimensões. A instrução seguinte permite declarar uma variável m para guardar
matrizes 4x3:

integer, dimension (4,3) :: m

ou, alternativamente, como para vectores:

integer, dimension (1:4,1:3) :: m

A função pré-definida size continua a poder ser aplicada a matrizes. Neste caso, há que utilizar um
segundo argumento indicando a dimensão de que se pretende calcular o tamanho, isto é, size(m,1)
permite obter o tamanho da dimensão 1, ou seja, o número de linhas (no caso do exemplo, 4),
size(m,2) permite obter o tamanho da dimensão 2, ou seja, o número de colunas (no caso do exemplo,
3). Esta função pode ser aplicada a estruturas de qualquer dimensão.

Como primeiro exemplo, apresenta-se uma função para somar todos os elementos de uma matriz.

function somaEl(m) result(x)


real, dimension(:,:), intent(in) :: m
real :: x
integer :: i, j

x=0
do i=1,size(m,1)
do j=1,size(m,2)
x=x+m(i,j)

20
end do
end do
end function somaEl

Esta função recorre a dois ciclos encaixados, o primeiro para percorrer as linhas da matriz e o segundo
para percorrer as colunas. Para cada valor de i e j, soma-se o valor da entrada correspondente da matriz
a x. Esta função foi definida num módulo mmatrices, que inclui outras operações sobre matrizes,
apresentadas em seguida. O programa seguinte utiliza esta função para somar os elementos de uma
matriz.

program testsomaEl

real, dimension(3,2) :: m

m(1,1)=1
m(1,2)=3
m(2,1)=2
m(2,2)=1
m(3,1)=1.5
m(3,2)=2.5
print *,somaEl(m)

contains

function somaEl(m) result(x)


real, dimension(:,:), intent(in) :: m
real :: x
integer :: i, j

x=0
do i=1,size(m,1)
do j=1,size(m,2)
x=x+m(i,j)
end do
end do
end function somaEl
end program testsomaEl

Com efeito,

11.0000000

Tal como no caso dos vectores, podemos definir uma subrotina prodEscalar para multiplicar uma
matriz por um escalar. A chamada desta subrotina altera a matriz, ficando o argumento com o resultado
da multiplicação da matriz pelo escalar.

subroutine prodEscalar(m,x)
real, dimension(:,:), intent(inout) :: m
real, intent (in) :: x
integer :: i,j

do i=1,size(m,1)
do j=1,size(m,2)
m(i,j)=m(i,j)*x
end do
end do
end subroutine prodEscalar

Deixa-se como exercício resolver o problema anterior recorrendo a uma função e testar quer a subrotina
quer a função.

21
Considere-se agora o problema de somar duas matrizes. Pretendemos definir uma função que receba
como argumentos duas matrizes compatíveis (com o mesmo número de linhas e de colunas) e devolva
como resultado a matriz resultante da soma. Tal como no caso vectorial, há que declarar a matriz
resultado com as mesmas dimensões das matrizes argumento:

function somaM(m1,m2) result(a)


real, dimension(:,:), intent(in) :: m1,m2
real, dimension(size(m1,1),size(m1,2)) :: a

Este tipo de declaração (vectores e matrizes automáticas) já tinha sido utilizado anteriormente, para o
caso dos vectores. Note-se, no entanto, que este tipo de declaração apenas pode ser aplicada ao tamanho.
O número de dimensões está fixo, ou seja, no caso da matriz a anterior, esta tem sempre duas dimensões
(linhas e colunas). A função somaM que soma duas matrizes é a seguinte:

function somaM(m1,m2) result(a)


real, dimension(:,:), intent(in) :: m1,m2
real, dimension(size(m1,1),size(m1,2)) :: a
integer :: i,j

if ((size(m1,1)==size(m2,1)).and.(size(m1,2)==size(m2,2))) then
do i=1,size(m1,1)
do j=1,size(m1,2)
a(i,j)=m1(i,j)+m2(i,j)
end do
end do
else
print *,"Erro! As matrizes não são da mesma forma"
end if
end function somaM

O programa seguinte testa esta função:

program testsomaM

real, dimension(3,2) :: m1,m2,m

m1(1,1:2)=(/ 3,2 /)
m1(2,1:2)=(/ 2,1 /)
m1(3,1:2)=(/ 1.5,4.5 /)
m2(1,1:2)=(/ 4.5,3.0 /)
m2(2,1:2)=(/ 1.2,1.5 /)
m2(3,1:2)=(/ 2,3 /)
m=somaM(m1,m2)
print *,m(1,1),m(1,2)
print *,m(2,1),m(2,2)
print *,m(3,1),m(3,2)

contains

function somaM(m1,m2) result(a)


real, dimension(:,:), intent(in) :: m1,m2
real, dimension(size(m1,1),size(m1,2)) :: a
integer :: i,j

if ((size(m1,1)==size(m2,1)).and.(size(m1,2)==size(m2,2))) then
do i=1,size(m1,1)
do j=1,size(m1,2)
a(i,j)=m1(i,j)+m2(i,j)
end do
end do
else

22
print *,"Erro! As matrizes não são da mesma forma"
end if
end function somaM
end program testsomaM

Com efeito,

7.5000000 5.0000000
3.2000000 2.5000000
3.5000000 7.5000000

Alternativamente, podemos optar por definir uma subrotina que some as duas matrizes, alterando a
primeira, que fica com o resultado da soma.

subroutine somaM(m1,m2)
real, dimension(:,:), intent(inout) :: m1
real, dimension(:,:), intent(in) :: m2
integer :: i,j

if ((size(m1,1)==size(m2,1)).and.(size(m1,2)==size(m2,2))) then
do i=1,size(m1,1)
do j=1,size(m1,2)
m1(i,j)=m1(i,j)+m2(i,j)
end do
end do
else
print *,"Erro! As matrizes não são da mesma forma"
end if
end subroutine somaM

Neste caso, o resultado da soma de m1 e m2 é deixado em m2, como se comprova com o programa
seguinte:

program testsomaM1

real, dimension(3,2) :: m1,m2

m1(1,1:2)=(/ 3,2 /)
m1(2,1:2)=(/ 2,1 /)
m1(3,1:2)=(/ 1.5,4.5 /)
m2(1,1:2)=(/ 4.5,3.0 /)
m2(2,1:2)=(/ 1.2,1.5 /)
m2(3,1:2)=(/ 2,3 /)
call somaM(m1,m2)
print *,m1(1,1),m1(1,2)
print *,m1(2,1),m1(2,2)
print *,m1(3,1),m1(3,2)

contains

subroutine somaM(m1,m2)
real, dimension(:,:), intent(inout) :: m1
real, dimension(:,:), intent(in) :: m2
integer :: i,j

if ((size(m1,1)==size(m2,1)).and.(size(m1,2)==size(m2,2))) then
do i=1,size(m1,1)
do j=1,size(m1,2)
m1(i,j)=m1(i,j)+m2(i,j)
end do

23
end do
else
print *,"Erro! As matrizes não são da mesma forma"
end if
end subroutine somaM
end program testsomaM1

7.5000000 5.0000000
3.2000000 2.5000000
3.5000000 7.5000000

Considere-se agora o problema de pesquisar um elemento numa matriz. A primeira solução capitaliza no
facto de pesquisar numa linha ser equivalente a pesquisar num vector. Assim, a solução consiste em
utilizar a uma função de pesquisa em vector e aplicá-la a cada uma das linhas.

function pesquisaV(x,v) result(b)


real, intent(in) :: x
real, dimension(:), intent(in) :: v
logical :: b
integer :: j

b=.false.
do j=1,size(v)
if (x==v(j)) then
b=.true.
exit
end if
end do
end function pesquisaV

Esta função percorre os elementos do vector um a um até encontrar o elemento x e, caso encontre, coloca
a variável resultado a .true. e termina a execução do ciclo do (atraves do comando exit). Podemos
agora utilizar esta função para pesquisar numa linha de uma matriz. Podemos referir-nos a uma
determinada linha i de uma matriz m através da expressão:

m(i,1:size(m,2))

assim como nos podemos referir a uma determinada coluna j da mesma matriz através da expressão

m(1:size(m,1),j)

Assim, pesquisar se um elemento ocorre numa matriz consiste em pesquisar se ele ocorre em alguma das
linhas e, caso ocorra, terminar a pesquisa com sucesso. Apresenta-se em seguida uma função para
pesquisar um elemento numa matriz, utilizando o método descrito:

function pesquisa(x,m) result(b)


real, dimension(:,:), intent(in) :: m
real, intent(in) :: x
logical :: b
integer :: i

b=.false.
do i=1,size(m,1)
if (pesquisaV(x,m(i,1:size(m,2)))) then
b=.true.
exit
end if
end do
end function pesquisa

24
Alternativamente, podíamos ter optado por definir a função de pesquisa anterior sem recorrer à função
auxiliar sobre vectores. Esta solução tem a vantagem de não duplicar informação, que resulta de calcular
cada uma das linhas para em seguida lhes aplicar a função pesquisaV. Neste caso, há que recorrer a
dois ciclos. A pesquisa termina quando se percorrer a matriz toda ou quando se encontrar o elemento.

function pesquisa(x,m) result(b)


real, dimension(:,:), intent(in) :: m
real, intent(in) :: x
logical :: b
integer :: i, j

b=.false.
do i=1,size(m,1)
do j=1,size(m,2)
if (x==m(i,j)) then
b=.true.
exit
end if
end do
if (b) then
exit
end if
end do
end function pesquisa

Por fim, considere-se a função para multiplicar duas matrizes, desde que estas sejam compatíveis. Tal
como no exemplo anterior, começamos por apresentar uma solução que recorre a uma função auxiliar
para calcular o produto interno de uma linha por uma coluna, ou seja, uma função que recebe dois
vectores do mesmo tamanho e calcula o respectivo produto interno.

function prodLC(l,c) result(x)


real, dimension(:), intent(in) :: l,c
real :: x
integer :: k

x=0
do k=1,size(l)
x=x+l(k)*c(k)
end do
end function prodLC

Recorrendo a esta função, podemos definir o produto de duas matrizes, desde que estas sejam
compatíveis, calculando para cada (i,j) o produto interno da linha i da primeira matriz pela coluna j
da segunda matriz.

function prodM(m1,m2) result(a)


real, dimension(:,:), intent(in) :: m1,m2
real, dimension(size(m1,1),size(m2,2)) :: a
integer :: i,j

if (size(m1,2)==size(m2,1)) then
do i=1,size(m1,1)
do j=1,size(m2,2)
a(i,j)=prodLC(m1(i,1:size(m1,2)),m2(1:size(m2,1),j))
end do
end do
else
print *,"Erro! As matrizes nao sao compativeis"
end if
end function prodM

25
Alternativamente, podemos definir a função directamente, como se ilustra em seguida:

function prodM(m1,m2) result(a)


real, dimension(:,:), intent(in) :: m1,m2
real, dimension(size(m1,1),size(m2,2)) :: a
real :: x
integer :: i,j,k

if (size(m1,2)==size(m2,1)) then
do i=1,size(m1,1)
do j=1,size(m2,2)
x=0
do k=1,size(m1,2)
x=x+m1(i,k)*m2(k,j)
end do
a(i,j)=x
end do
end do
else
print *,"Erro! As matrizes nao sao compativeis"
end if
end function prodM

Deixa-se como exercício testar as funções anteriores.

Programação recursiva

Apresentam-se em seguida algumas das funções sobre vectores apresentadas anteriormente, mas
desenvolvidas num estilo recursivo. Como já se viu, uma função ou subrotina recursiva deve ser
declarada como tal, ou seja, com a declaração recursive.

Como primeiro exemplo, considere-se o problema de somar os elementos de um vector. A função


somaVecR recebe como argumento um vector e devolve a soma dos seus elementos. Utiliza a função
recursiva auxiliar somaVecAux, que tem dois argumentos, um vector e uma posição, e calcula a soma
dos elementos do vector a partir da posição dada. Note-se que a soma dos elementos de um vector pode
ser definida recursivamente, observando que a soma dos elementos a partir de uma dada posição é o
resultado de somar o elemento que está nessa posição com a soma dos elementos nas posições seguintes,
isto é:

somaVecAux(v,i)=v(i)+somaVecAux(v,i+1)

Claro que a expressão anterior apenas se verifica se i for menor ou igual do que o comprimento do
vector. Caso exceda este valor, a soma dos elementos do vector deverá ser 0.

Apresenta-se em seguida a definição destas duas funções:

function somaVec(v) result(x)


integer, dimension(:), intent(in) :: v
integer :: x

x=somaVecAux(v,1)
end function somaVec

recursive function somaVecAux(v,i) result(x)


integer, dimension(:), intent(in) :: v
integer, intent(in) :: i
integer :: x

if (i>size(v)) then
x=0
else

26
x=v(i)+somaVecAux(v,i+1)
end if
end function somaVecAux

Deixa-se como exercício testar esta função.

Considera-se em seguida o problema de calcular o produto interno de dois vectores. Novamente,


considera-se uma função prodInterno, que recorre a uma função recursiva auxiliar prodIntAux
para calcular o produto interno de dois vectores a partir de uma determinada posição. Observa-se
facilmente que o problema de calcular o produto interno a partir de uma determinada posição pode ser
definido recursivamente somando o produto da posição que está a ser considerada dois vectores com o
produto interno dos dois vectores a partir da posição seguinte:

prodIntAux(v1,v2,i)=v1(i)*v2(i)+prodIntAux(v1,v2,i+1)

Novamente, a condição anterior só se verifica se i for menor do que o comprimento dos dois vectores. A
função principal deverá começar por testar se os dois vectores são do mesmo tamanho.

function prodInterno(v1,v2) result(x)


integer, dimension(:), intent(in) :: v1,v2
integer :: x

if (size(v1)/=size(v2)) then
print *,"Erro! Os vectores nao sao do mesmo tamanho."
else
x=prodIntAux(v1,v2,1)
end if
end function prodInterno

recursive function prodIntAux(v1,v2,i) result(x)


integer, dimension(:), intent(in) :: v1,v2
integer, intent(in) :: i
integer :: x

if (i>size(v1)) then
x=0
else
x=v1(i)*v2(i)+prodIntAux(v1,v2,i+1)
end if
end function prodIntAux

Em seguida considera-se o problema de pesquisar um número num vector ordenado. Neste caso, recorre-
se ao algoritmo de pesquisa binária: sabendo que o vector está ordenado (por ordem crescente), para
procurar um elemento x nesse vector basta comparar o elemento com o elemento no meio do vector. Uma
de três situações pode acontecer: x é igual ao elemento do meio, e nesse caso termina-se a pesquisa com
sucesso; x é maior do que o elemento do meio, e nesse caso basta pesquisar do elemento do meio para a
direita; x é menor do que o elemento do meio, e neste caso apenas é necessário pesquisar do elemento do
meio para a esquerda. Esta pesquisa é feita recursivamente. Neste caso, no entanto, há que considerar
duas posições do vector delimitando os extremos do intervalo do vector onde está a ser efectuada a
pesquisa (que podem não coincidir com os extremos do vector). Apresenta-se em seguida a definição da
função recursiva para pesquisar se um determinado elemento ocorre entre duas posições de um vector
ordenado:

recursive function pesquisaAux(x,v,p,u) result(b)


integer, intent(in) :: x,p,u
integer, dimension(:), intent(in) :: v
logical :: b
integer :: m

if (u<p) then
b=.false.

27
else
m=(p+u)/2
if (x==v(m)) then
b=.true.
elseif (x>v(m)) then
b=pesquisaAux(x,v,m+1,u)
else
b=pesquisaAux(x,v,p,m-1)
end if
end if
end function pesquisaAux

Esta função é utilizada pela função de pesquisa binária:

function pesquisa(x,v) result(b)


integer, intent(in) :: x
integer, dimension(:), intent(in) :: v
logical :: b

b=pesquisaAux(x,v,1,size(v))
end function pesquisa

Finalmente, conisdera-se o problema de multiplicar um vector por um escalar. Neste caso, optou-se por
definir uma subrotina recursiva que altera directamente o vector argumento. Para tal, é necessário definir
o parâmetro correspondente ao vector como sendo de entrada e saída (inout). De resto, a subrotina
recursiva é em tudo semelhante à função para somar os elementos de um vector.

recursive subroutine prodEscalarAux(v,x,i)


integer, dimension(:), intent(inout) :: v
integer, intent(in) :: x,i

if (i==size(v)) then
v(i)=v(i)*x
else
v(i)=v(i)*x
call prodEscalarAux(v,x,i+1)
end if
end subroutine prodEscalarAux

A função que calcula o produto escalar utiliza a função auxiliar anterior:

subroutine prodEscalar(v,x)
integer, dimension(:), intent(inout) :: v
integer, intent(in) :: x

call prodEscalarAux(v,x,1)
end subroutine prodEscalar

recursive subroutine prodEscalarAux(v,x,i)


integer, dimension(:), intent(inout) :: v
integer, intent(in) :: x,i

if (i==size(v)) then
v(i)=v(i)*x
else
v(i)=v(i)*x
call prodEscalarAux(v,x,i+1)
end if
end subroutine prodEscalarAux

28
Capítulo 3 Unidades de programa: módulos
Objectivos
Unidades de programa: módulos. Funções como argumento de funções.

Unidades de programa

A definição de funções e subrotinas em F pode ser feita dentro do corpo principal do programa, como foi
ilustrado atrás, ou fora deste, numa unidade à parte chamada módulo. Um módulo é uma unidade de
programa que contém, entre outras, definições de funções e subrotinas, e que não pode ser executada
directamente, podendo apenas ser utilizada por outros programas. A vantagem de um módulo é que pode
ser utilizado por diferentes programas sem necessidade de repetir o código. Considerem-se as funções
sobre matrizes definidas no capítulo anterior. Podemos definir um módulo que disponibilize estas funções
e que poderá ser utilizado por um ou mais programas que necessitem de algumas dessas funções e
subrotinas sem ser necessário repetir o respectivo código.

Apresenta-se em seguida um módulo que disponibiliza algumas funções sobre matrizes.

module mmatrices

public :: somaEl, prodEscalar, somaM, prodM, pesquisa


private :: prodLC

contains

function somaEl(m) result(x)


real, dimension(:,:), intent(in) :: m
real :: x
integer :: i, j

x=0
do i=1,size(m,1)
do j=1,size(m,2)
x=x+m(i,j)
end do
end do
end function somaEl

function pesquisa(x,m) result(b)


real, dimension(:,:), intent(in) :: m
real, intent(in) :: x
logical :: b
integer :: i, j

b=.false.
do i=1,size(m,1)
do j=1,size(m,2)
if (x==m(i,j)) then
b=.true.
exit
end if
end do
if (b) then
exit
end if
end do
end function pesquisa

subroutine prodEscalar(m,x)
real, dimension(:,:), intent(inout) :: m

29
real, intent (in) :: x
integer :: i,j

do i=1,size(m,1)
do j=1,size(m,2)
m(i,j)=m(i,j)*x
end do
end do
end subroutine prodEscalar

function somaM(m1,m2) result(a)


real, dimension(:,:), intent(in) :: m1,m2
real, dimension(size(m1,1),size(m1,2)) :: a
integer :: i,j

if ((size(m1,1)==size(m2,1)).and.(size(m1,2)==size(m2,2))) then
do i=1,size(m1,1)
do j=1,size(m1,2)
a(i,j)=m1(i,j)+m2(i,j)
end do
end do
else
print *,"Erro! As matrizes não são da mesma forma"
end if
end function somaM

function prodM(m1,m2) result(a)


real, dimension(:,:), intent(in) :: m1,m2
real, dimension(size(m1,1),size(m2,2)) :: a
integer :: i,j

if (size(m1,2)==size(m2,1)) then
do i=1,size(m1,1)
do j=1,size(m2,2)
a(i,j)=prodLC(m1(i,1:size(m1,2)),m2(1:size(m2,1),j))
end do
end do
else
print *,"Erro! As matrizes nao sao compativeis"
end if
end function prodM

function prodLC(l,c) result(x)


real, dimension(:), intent(in) :: l,c
real :: x
integer :: k

x=0
do k=1,size(l)
x=x+l(k)*c(k)
end do
end function prodLC
end module mmatrices

O modulo anterior é constituido por duas partes: a primeira parte, entre o início do módulo e a instrução
contains; a segunda parte, desde a instrução contains até ao fim do módulo.

Na primeira parte declaram-se eventuais módulos que este módulo possa utilizar, funções e subrotinas
que o módulo disponibiliza (através da instrução public), funções e subrotinas auxiliares (através da
instrução private), declarações de variáveis, entre outras de que veremos exemplos adiante. No caso

30
do exemplo, o módulo apenas disponibiliza as funções e subroutines somaEl, prodEscalar, somaM,
prodM e pesquisa, já descritas anteriormente, através da declaração

public :: somaEl, prodEscalar, somaM, prodM, pesquisa

e define ainda uma função auxiliar, prodLC, que embora seja implementada no módulo não pode ser
utilizada fora deste pois foi declarada como privada, através da declaração

private :: prodLC

Na segunda parte, definem-se as funções e subrotinas do módulo, quer as que são disponibilizadas pelo
módulo quer as auxiliares. As funções auxiliares apenas podem ser referênciadas dentro do módulo, por
outras funções, não sendo conhecidas fora deste. No caso do módulo anterior, a função prodLC apenas
pode ser utilizada dentro do módulo (como de facto é, pela função prodM), não podendo ser utilizada por
nenhum programa que utilize este módulo.

Um programa pode utilizar um módulo através da instrução use. O programa seguinte utiliza o módulo
mmatrices para multiplicar duas matrizes.

program testeProd

use mmatrices

real, dimension(2,2) :: m1,m2,m3

m1(1,1:2)=(/ 1.0,2.5 /)
m1(2,1:2)=(/ 4.5,1.1 /)
m2(1,1:2)=(/ 1.3,2.3 /)
m2(2,1:2)=(/ 5.5,0.5 /)
m3=prodM(m1,m2)
print *,m3(1,1:2)
print *,m3(2,1:2)
end program testeProd

Ao utilizar o módulo mmatrices, o programa anterior tem à disposição todas as funções


disponibilizadas (públicas) por este módulo.

A compilação do programa anterior é, no entanto, um pouco mais complexa. Em primeiro lugar, há que
compilar o módulo. Como o resultado não é um ficheiro executável, há que indicar isso aquando da
compilação, através da opção –c. A instrução anterior gera um ficheiro mmatrices.o. Cada módulo só
precisa de ser compilado quando é criado e de cada vez que é alterado (mas não quando é utilizado). Uma
vez compilado o módulo, podemos compilar o programa. O processo de compilação é semelhante ao visto
anteriormente, sendo apenas necessário acrescentar o nome do(s) módulo(s) que o programa utiliza.

A estrutura genérica de um módulo é a seguinte:

module nome
[use módulos]
public :: nomes
private :: nomes
outras declarações

contains
definição de funções e subrotinas
end module nome

Funções como argumento de procedimentos

31
Tal como foi visto anteriormente, é muitas vezes necessário definir funções ou subrotinas cujos
parâmetros são eles próprios funções. Pretende-se definir uma função integrate que recebe como
argumentos uma função real de variável real f e dois números reais a e b e um número inteiro n e calcula
o integral de f no intervalo [a,b] (dividido em n subintervalos), tal como foi descrito anteriormente. A
declaração de um parâmetro de tipo função é feita através da declaração da interface da função, onde se
indicam quais os tipos dos parâmetros e qual o tipo do resultado. No caso presente, pretende-se que o
parâmetro f seja uma função com um parâmetro real e devolvendo um número real, o que é obtido
através da declaração seguinte:

interface
function f(x) result(y)
real, intent(in) :: x
real :: y
end function f
end interface

Uma solução possível para a função integrate, disponibilizada pelo módulo mnumeric.f95, é a
seguinte:

function integrate(f,a,b,n) result(y)


interface
function f(x) result(y)
real, intent(in) :: x
real :: y
end function f
end interface
real, intent(in) :: a,b
integer, intent(in) :: n
real :: y
real:: d,x,s
integer :: i

d=(b-a)/n
s=0
x=a
do i=1,n
s=s+f(x)
x=x+d
end do
y=d*s
end function integrate

O funcionamento desta função já foi descrito anteriormente. A principal diferença reside na necessidade
de declaração dos parâmetros e das variáveis auxiliares e, em particular, na declaração do parâmetro f.
Repare-se que, uma vez declarado, este parâmetro pode ser usado como uma função real de variável real,
como acontece na expressão s=s+f(x).

O programa seguinte utiliza esta função para calcular o integral da função quad (definida pela expressão
analítica -x2-x+2), disponibilizada no módulo mquad.

program testintegrate

use mnumeric
use mquad

real :: a,b
integer :: n

read *,a
read *,b

32
read *,n
print *,"O integral da funcao -x^2-x+2"
print *,"no intervalo [",a,",",b,"]"
print *,"e: ",integrate(quad,a,b,n)

end program testintegrate

O programa solicita ao utilizador os extremos do intervalo bem como o número de subdivisões pretendido
e calcula o respectivo integral, de acordo com o método apresentado.

Seguem-se alguns exemplos de execução:

-2.0
2.0
100
O integral da funcao -x^2-x+2
no intervalo [ -2.0000000 , 2.0000000 ]
e: 2.7456055

-3.0
3.0
10000
O integral da funcao -x^2-x+2
no intervalo [ -3.0000000 , 3.0000000 ]
e: -5.9991608

Note-se que a função que vai ser passada como argumento (neste caso a função quad) tem que estar
definida num módulo diferente do da função que a vai receber como parâmetro (neste caso, a função
integrate). Com efeito, a função quad está definida no módulo mquad e a função integrate está
definida no módulo mnumeric.

Capítulo 4 Programação em grande escala


Objectivos

Objecto como área de memória. Objectos versus valores. Tipos de objectos. Método de programação
modular por camadas baseadas em objectos. Primeiro exemplo: torres de Hanoi sobre camada disponi-
bilizando pilhas de inteiros. Utilização dos módulos de F para definir camadas. Implementação das pilhas
com vectores. Apontadores em F. Implementação das pilhas com apontadores. Vectores dinâmicos em F.
Implementação das pilhas com vectores dinâmicos. Exemplo complementar: filas de espera implemen-
tadas com apontadores.

Programação modular por camadas baseadas em objectos

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.

33
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;
 emptyQ(s) é uma função que testa se a pilha s está vazia.

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

34
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

35
| 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).]

36
 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

37
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)
type(stack), intent(in) :: 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

38
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)

39
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

40
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))

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

Objectos dinâmicos

No caso de um objecto estático (como todos os que vimos até agora), desde os mais pequenos (por
exemplo uma variável inteira) aos muito grandes (por exemplo, um vector com um milhão de
componentes), o seu tamanho (espaço na memória) fica fixado no momento em que o programa é
compilado e o espaço em memória necessário é afectado ao programa no momento em que a execução
arranca, sendo libertado quando a execução termina. [No caso de variáveis locais de funções e
procedimentos (eventualmente com chamadas recursivas) os detalhes do processo de afectação de
memória são um pouco mais complexos mas não vale a pena neste momento analisá-los.]

Mas há situações em que os objectos estáticos não são satisfatórios. Com efeito, considere-se um
problema em que seja preciso trabalhar com pilhas de inteiros cuja profundidade não é limitada à partida
por qualquer condicionante. Para ilustração (mas sem entrar em pormenores) considere-se o problema de
verificar se numa expressão matemática (de comprimento arbitrário) os parênteses estão bem usados (isto
é, a todo o parêntesis esquerdo corresponde um parêntesis direito do mesmo tipo).

42
Em tais problemas, a camada das pilhas implementadas sobre vectores de comprimento fixo (64)
introduzida na secção anterior não serve. A solução passa por poder criar objectos dinâmicos durante a
execução do programa cujo tamanho possa variar até que o objecto seja destruído ou a execução do
programa termine.

No caso das pilhas, considere-se a seguinte ideia para a sua implementação dinâmica na memória:

node node node node

11 22 33 44

stack

O primeiro nó (o que é apontado por stack) contém o elemento no topo da pilha. Fazer um push
corresponde a criar um objecto de tipo nó (node) e pô-lo a apontar para o nó que antes era o topo da
pilha. Suponha-se que se declarou uma variável de tipo pilha e que o sistema está num estado em que o
conteúdo dessa variável s é:

11 22 33 44

variável s

então, após a execução de push(55,s) o estado do sistema deverá ser tal que o conteúdo da memória
seja:

11 22 33 44 55

variável s

Fazer um pop corresponde a pôr o apontador pilha (stack) a apontar para o nó que antes estava a seguir
ao topo (e libertar a memória ocupada pelo nó que anteriormente era o topo).

Considere-se que o sistema se encontra num estado em que o conteúdo da variável s é:

43
11 22 33 44 55

variável s

então, após a execução de pop(s), o estado do sistema deverá ser tal que o conteúdo da variável s seja:

11 22 33 44 55

variável s

ou seja, o nó correspondente ao topo de s (55) foi apagado e o nó seguinte (44) passou a ser o novo topo.

Note-se que esta implementação das pilhas envolve a criação e destruição de objectos durante a execução
do programa (os nós são criados e destruídos à medida que se vai fazendo push e pop). Mais ainda,
envolve também a utilização de objectos cujo tamanho varia ao longo da execução do programa (as pilhas
vão crescendo e diminuindo, sem limite fixado para a sua profundidade, à medida que se vai fazendo
push e pop).

No módulo seguinte definimos o tipo pilha de inteiros como um tipo de objecto dinâmico (no caso uma
cadeia de nós encadeados).

module mustacks

public :: new, push, pop, top, emptyQ, erase, show

private :: rev

type, public :: stack


private
type(node), pointer :: chain
integer :: dep
end type stack

type, private :: node


integer :: value
type(node), pointer :: next
end type node

contains

function new() result(r)


type(stack) :: r

r%dep=0
end function new

subroutine push(x,s)

44
integer, intent(in) :: x
type(stack), intent(inout) :: s
type(node), pointer :: aux

allocate(aux)
aux%value = x
aux%next => s%chain
s%chain => aux
s%dep=s%dep+1
end subroutine push

subroutine pop(s)
type(stack), intent(inout) :: s
type(node), pointer :: aux

if (s%dep>0) then
aux => s%chain
s%chain => s%chain%next
deallocate(aux)
s%dep=s%dep-1
end if
end subroutine pop

function top(s) result(y)


type(stack), intent(in) :: s
integer :: y

if (s%dep>0) then
y = s%chain%value
end if
end function top

function emptyQ(s) result(b)


type(stack), intent(in) :: s
logical :: b

if (s%dep>0) then
b = .false.
else
b = .true.
end if
end function emptyQ

recursive subroutine erase(s)


type(stack), intent(inout) :: s
type(node), pointer :: aux

if (s%dep>0) then
aux => s%chain
s%chain => s%chain%next
deallocate(aux)
s%dep=s%dep-1
call erase(s)
end if
end subroutine erase

recursive subroutine rev(s,r,d)


type(stack), intent(in) :: s
type(stack), intent(inout) :: r
integer, intent(in) :: d
type(stack) :: aux

45
if (d>0) then
call push(top(s),r)
aux%chain => s%chain%next
call rev(aux,r,d-1)
end if
end subroutine rev

subroutine show(s)
type(stack), intent(in) :: s
type(stack) :: r
integer :: i
integer, dimension(s%dep) :: aux

r=new()
call rev(s,r,s%dep)
do i = 1,s%dep
aux(i) = top(r)
call pop(r)
end do
print *, "|", aux
end subroutine show
end module mustacks

Tal como no exemplo anterior, é conveniente comentar cada umas das componentes deste módulo.
Comecemos pela declaração do tipo node:

type, private :: node


integer :: value
type(node), pointer :: next
end type node

De acordo com o que foi dito atrás, um nó é composto por duas componentes: um número inteiro,
referenciado por value, e uma referência (apontador) para o próximo nó da cadeia referenciada por
next.

Suponha-se que se tem uma variável n que é um apontador para node. Suponha-se ainda que se pretende
armazenar em n o inteiro 34 e ligar este nó a uma cadeia de nós já existente. O primeiro passo consiste
em criar espaço em memória para um objecto deste tipo, através da instrução (note-se que n é uma
variável de tipo node)

allocate(n)

Esta instrução cria um local em memória para armazenar um inteiro e um apontador para node:

node

value

next

Em seguida, instanciam-se os campos da variável. Começa-se pelo campo value. A instrução

n%value = 34

origina a seguinte mudança de estado:

46
node

34 value

next

Em seguida, assumindo que a cadeia à qual se pretende ligar este nó está guardada numa variável m, a
instrução

n%next=>m

permite fazer a desejada ligação:

34
...

m n

A instrução anterior significa colocar o apontador n%next a apontar para o objecto apontado por m. Tal é
conseguido através da instrução => (e nunca através de uma atribuição normal).

A remoção de um objecto de memória é feita através da instrução deallocate. Considere-se o sistema


num estado em que o conteúdo das variáveis seja o descrito pela figura anterior. Ao executar a instrução

deallocate(n)

apaga-se o objecto apontado por n. Assim, o sistema ficará num estado em que o conteúdo das variáveis
m e n é o seguinte:

34
...

m n

Ou seja, o nó apontado por n, que continha o número 34, deixou de existir em memória e a variável n,
embora continue a existir, passa a apontar para null. Ao tentar aceder, por exemplo, a n%next%value
ocorre num erro que termina a execução do programa.

É importante realçar que um apontador para um objecto é exactamente isso, uma maneira de referir esse
objecto, que existe numa determinada posição de memória. Considere-se a situação anterior, em que
existem duas variáveis m e n, apontadores para node e que o sistema se encontra num estado em que o
conteúdo das variáveis é o seguinte:

25 34
...

m n

47
Em particular, o conteúdo de n%next%value é 25. No entanto, após a execução da instrução

m%value=20

o sistema passa para um estado em que o conteúdo das variáveis é:

20 34
...

m n

Não é difícil concluir que, neste caso, o conteúdo de n%next%value é 20. Ou seja, mesmo sem
modificar directamente o valor da variável n, o objecto por ela apontado (a cadeia de nós) foi alterado.

É preciso especial cuidado que se apaga um objecto. Em particular, podemos ter duas variáveis a apontar
para o mesmo objecto. Se se apagar o objecto apontado por uma delas está-se a apagar também o objecto
apontado pela outra (são o mesmo). Imagine-se a seguinte situação:

34

m n

Neste caso, as variáveis m e n apontam para o mesmo objecto, o nó com o valor 34. Ao apagar o objecto
apontado por n, através da instrução

deallocate(n)

o sistema passa para o estado

34

m n

ou seja, o objecto apontado por m (que era o mesmo que n apontava) também foi apagado. No entanto,
enquanto a variável n, apagada através da instrução deallocate, ficou a apontar para null, a variável
m, que não foi apagada explicitamente, ficou desassociada (dangling), o que é uma situação que se deve
evitar, pois referências a m podem originar resultados imprevisíveis. A maneira de evitar estas situações,
como já não podemos apagar o objecto apontado por m, consiste em colocar m a apontar para null
explicitamente através da instrução:

m=>null()

A função associated permite testar se um apontador está associado a um objecto. Esta função devolve
.true. se o apontador estiver associado a um objecto, .false. se estiver a apontar para null e dá
erro caso o apontador não esteja associado.

Regressemos ao exemplo das pilhas dinâmicas. Tendo definido o tipo nó (node), uma pilha é composta
por um apontador para um destes nós e por um inteiro indicando a profundidade da pilha. Este segundo

48
campo, embora opcional, permite implementar as funções e subrotinas de uma forma mais eficiente. Tal
como no caso anterior, pretende-se que a estrutura interna do tipo não seja conhecida fora do módulo.

type, public :: stack


private
type(node), pointer :: chain
integer :: dep
end type stack

Analisemos agora cada uma das funções disponíveis para manipular este tipo. Comecemos pela função
new. Tal como foi referido, esta função devolve a pilha vazia. Uma pilha vazia corresponde a um
apontador para null e com profundidade 0.

function new() result(r)


type(stack) :: r

r%dep=0
r%chain=>null()
end function new

A subrotina push recebe dois argumentos, uma pilha s e um inteiro x que se pretende sobrepor à pilha.

subroutine push(x,s)
integer, intent(in) :: x
type(stack), intent(inout) :: s
type(node), pointer :: aux

allocate(aux)
aux%value = x
aux%next => s%chain
s%chain => aux
s%dep=s%dep+1
end subroutine push

Tal como descrito no início da secção, o primeiro passo consiste em criar um novo nó, recorrendo, neste
caso, à variável auxiliar aux (allocate(aux)):

s aux

em seguida, guardar x no campo value de aux (aux%value=x) e pôr o campo next a apontar para o
objecto apontado por s (aux%next=>s%chain):

s aux

49
Finalmente, colocar s a apontar para o objecto apontado por aux (s%chain=>aux):

s aux

No fim, incrementa-se a profundidade da pilha.

A subrotina pop recebe uma pilha s e retira o elemento no seu topo, caso exista.

subroutine pop(s)
type(stack), intent(inout) :: s
type(node), pointer :: aux

if (s%dep>0) then
aux => s%chain
s%chain => s%chain%next
deallocate(aux)
s%dep=s%dep-1
end if
end subroutine pop

Se a pilha não estiver vazia, ou seja, se a profundidade não for 0, então apaga-se o elemento no topo da
pilha (um teste alternativo seria testar se s%chain está associado ou não). Para tal, coloca-se uma
variável auxiliar aux a apontar para o objecto apontado por s (aux=>s%chain):

s aux

Em seguida, coloca-se s a apontar para o elemento seguinte da cadeia (s%chain=>s%chain%next):

s aux

e liberta-se o espaço de memória até agora ocupado pelo topo da pilha (deallocate(aux)):

50
x

s aux

Por fim, decrementa-se a profundidade da pilha. Note-se que é fundamental colocar aux a apontar para o
objecto que se pretende apagar antes de avançar s. Se tal não fosse feito perdia-se a referência a este
elemento e não seria possível apagá-lo.

A função top recebe como argumento uma pilha s e devolve o elemento no seu topo, caso exista. Se a
pilha não estiver vazia, ou seja, se s%chain estiver associado a um objecto, o elemento no topo da pilha
é o valor que se encontra no campo value do nó apontado por s%chain, isto é, s%chain%value:

function top(s) result(y)


type(stack), intent(in) :: s
integer :: y

if (s%dep>0) then
y = s%chain%value
end if
end function top

A função emptyQ recebe como argumento uma pilha s e testa se está vazia, ou seja, se a profundidade é
0.

function emptyQ(s) result(b)


type(stack), intent(in) :: s
logical :: b

if (s%dep>0) then
b = .false.
else
b = .true.
end if
end function emptyQ

A subrotina erase recebe como argumento uma pilha e apaga-a da memória.

recursive subroutine erase(s)


type(stack), intent(inout) :: s
type(node), pointer :: aux

if (s%dep>0) then
aux => s%chain
s%chain => s%chain%next
deallocate(aux)
s%dep=s%dep-1
call erase(s)
end if
end subroutine erase

Neste caso é necessário libertar a memória ocupada por cada um dos nós. Uma solução consiste em
percorrer a cadeia de nós recursivamente, libertando a memória ocupada por cada nó. O processo de
libertação de cada nó é semelhante ao usado na definição da subrotina pop.

51
Deixa-se como exercício a análise das outras funções apresentadas.

Tal como no caso anterior, 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 uhanoi

use mustacks

type(stack), dimension(3) :: t
integer :: n,k

n = 5
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 uhanoi

52
Deixa-se como exercício a comparação desta versão do programa Hanoi com a apresentada na secção
anterior.

Tabelas dinâmicas

Na linguagem F existe outra forma de introduzir objectos dinâmicos que não envolve a utilização de
apontadores. Com efeito, é possível trabalhar com objectos tabulares dinâmicos (vectoriais, matriciais,
etc) cujo tamanho (size e bounds) é fixado só no momento do arranque do programa ou da invocação da
subrotina/função. Mas a dimensão (rank) é fixa.

Como primeiro exemplo considere-se a seguinte função que permite calcular o produto interno de dois
vectores compatíveis de tamanho arbitrário:

program dintprod

real, dimension(:), allocatable :: u, v


real :: r

allocate(u(5))
allocate(v(5))
call random_number(u)
call random_number(v)
print *, "u = ",u
print *, "v = ",v
r=dip(u,v)
print *, "uv= ",r
print *, "----------------------------------------------------"
deallocate(u)
deallocate(v)

allocate(u(3))
allocate(v(3))
call random_number(u)
call random_number(v)
print *, "u = ",u
print *, "v = ",v
r=dip(u,v)
print *, "uv= ",r
print *, "----------------------------------------------------"
deallocate(u)
deallocate(v)

allocate(u(5))
allocate(v(3))
call random_number(u)
call random_number(v)
print *, "u = ",u
print *, "v = ",v
r=dip(u,v)
print *, "uv= ",r
print *, "----------------------------------------------------"

contains

function dip(u,v) result(y)


real, dimension(:), intent(in) :: u, v
real :: y
integer :: i

if (size(u)==size(v)) then

53
y = 0
do i = 1,size(u)
y = y + u(i)*v(i)
end do
else
print *, "erro: vectores incompativeis"
end if
end function dip
end program dintprod

Na função dip, os parâmetros u e e v são declarados sem explicitar os limites

real, dimension(:), intent(in) :: u, v

Mas, sublinhe-se que a dimensão (rank) fica fixada (no caso, fixada em 1).

No programa testing, com a declaração

real, dimension(:), allocatable :: u, v

são declarados os vectores dinâmicos u e v.

Por exemplo, o comando

allocate(u(3))

afecta à variável u o espaço em memória correspondente a um vector de comprimento 3.

Por outro lado, por exemplo o comando

deallocate(u)

liberta o espaço em memória até aí afectado a u.

De cada fez que se faz uma afectação pode-se escolher um tamanho diferente, aliás, como é ilustrado
várias vezes no programa testing.

Vale a pena examinar o resultado da execução do programa testing:

u = 0.7351025 0.8685266 0.3265546 0.4028542 0.7707380


v = 0.7934320 0.2116396 2.6788054E-02 0.2268322 0.3685732
uv= 1.1512699
----------------------------------------------------
u = 0.6098294 0.4019983 0.3847913
v = 0.1866101 0.3559107 0.7905946
uv= 0.5610896
----------------------------------------------------
u = 0.5225899 0.1677345 0.1130701 0.3690634 0.8479596
v = 0.6562512 0.6141976 0.8192743
erro: vectores incompativeis
uv= 1.0460699E+34
----------------------------------------------------

No primeiro caso, foram criados dois vectores com o mesmo tamanho (5), que foram em seguida
instanciados aleatoriamente, e calculou-se o respectivo produto interno. Em seguida, criaram-se dois
vectores de tamanho 3, tendo sido previamente apagados os vectores de tamanho 5. O processo repetiu-
se. No último exemplo, foram criados dois vectores de tamanhos diferentes, o vector u com tamanho 5 e
o vector v com tamanho 3. Neste caso, não é possível calcular o produto interno. É escrita uma
mensagem de erro no ecrã e o valor apresentado corresponde ao valor que se encontrava guardado na
variável.

54
Como segundo exemplo de utilização de estruturas tabulares dinâmicas, considere-se a seguinte solução
alternativa para o problema das torres de Hanoi:

module mastacks

public :: new, push, pop, top, emptyQ, fullQ, erase, show

type, public :: stack


private
integer :: index
integer, dimension(:), allocatable :: array
end type stack

contains

function new(n) result(r)


integer, intent(in) :: n
type(stack) :: r

r%index = 0
allocate(r%array(n))
end function new

subroutine push(x,s)
integer, intent(in) :: x
type(stack), intent(inout) :: s

if (s%index < size(s%array)) 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

55
function fullQ(s) result(b)
type(stack), intent(in) :: s
logical :: b

if (s%index == size(s%array)) then


b = .true.
else
b = .false.
end if
end function fullQ

subroutine erase(s)
type(stack), intent(inout) :: s

deallocate(s%array)
end subroutine erase

subroutine show(s)
type(stack), intent(in) :: s

print *, "|", s%array(1:s%index)


end subroutine show
end module mastacks

As principais diferenças desta solução quando comparada com a primeira residem no tratamento do limite
do vector. Enquanto na primeira solução o limite estava fixado à partida (um vector de tamanho 64), neste
caso o limite só é conhecido em tempo de execução, sendo por isso necessário recorrer à função size.
De resto, a implementação das funções é muito semelhante à já apresentada para o caso de tamanho fixo.
Exceptua-se o caso da função new, à qual é necessário acrescentar um parâmetro indicando qual o
tamanho da pilha que se pretende criar. Esta alteração reflecte-se no programa Hanoi, visto que ao criar as
torres, na função setup, é necessário fornecer a respectiva dimensão.

program ahanoi

use mastacks

type(stack), dimension(3) :: t
integer :: n,k

n = 5
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(n)
t(2) = new(n)
t(3) = new(n)
k = 0
do i = n,1,-1

56
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 ahanoi

Note-se que no programa ahanoirun se fixa o número de discos (n) em 5. Por esta razão as pilhas são
criadas com tamanho 5. Se fixássemos outro valor de n, as pilhas seriam criadas com esse tamanho.

Exemplo adicional: Filas de espera

Para exercitar os conceitos acabados de introduzir a propósito do problema das torres de Hanoi, vamos
agora definir um outro tipo de objectos (o tipo das filas de espera de inteiros) recorrendo a uma
implementação dinâmica com apontadores.

As filas de espera surgem em muitos problemas de computação. Em particular são utilizadas no capítulo
seguinte num problema de simulação de tráfego. As filas (com disciplina FIFO – first in first out) 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 fila vazia;


 enter(x,q) é uma subrotina que quando invocada introduz o elemento x no fim da fila q;
 leave(q) é uma subrotina que quando invocada retira o primeiro elemento da fila q;
 first(q) é uma função que devolve o primeiro elemento da fila q;
 emptyQ(q) é uma função que testa se a fila q está vazia.
 lenght(q) é uma função que devolve o comprimento da fila q.

Podemos implementar dinamicamente as filas com apontadores do modo seguinte

último primeiro

44 33 22 11

fila

57
Neste é caso, é necessário considerar dois apontadores: um para o início da fila (o apontador do lado
direito na figura) e outro para o fim da fila (o apontador do lado esquerdo na figura). O primeiro é
necessário para calcular o primeiro elemento da fila e para o retirar. O segundo é necessário para
introduzir novos elementos na fila.

A entrada de novos elementos é feita do seguinte modo: suponha-se que se declarou uma variável de tipo
fila e que o sistema está num estado em que o conteúdo dessa variável q é

44 33 22 11

variável q

Então, após a execução de enter(55,q), o sistema deverá estar num estado em que o conteúdo da
variável q seja:

55 44 33 22 11

variável q

A saída de elementos processa-se da seguinte forma: suponha-se que o sistema está num estado em que o
conteúdo da variável q é:

55 44 33 22 11

variável q

então, após a execução de leave(q), o sistema deverá estar num estado em que o conteúdo da variável q
seja:

55 44 33 22 11

variável q

O módulo seguinte concretiza esta ideia:

module mqueues

58
public :: new, enter, leave, first, emptyQ, erase, length, show

type, public :: queue


private
type(node), pointer :: fst
type(node), pointer :: lst
integer :: len
end type queue

type, private :: node


integer :: value
type(node), pointer :: next
end type node

contains

function new() result(r)


type(queue) :: r

r%fst=>null()
r%lst=>null()
r%len=0
end function new

subroutine enter(x,q)
integer, intent(in) :: x
type(queue), intent(inout) :: q
type(node), pointer :: aux

allocate(aux)
aux%value=x
if (q%len>0) then
q%lst%next=>aux
else
q%fst=>aux
end if
q%lst=>aux
q%len=q%len + 1
end subroutine enter

subroutine leave(q)
type(queue), intent(inout) :: q
type(node), pointer :: aux

if (q%len>0) then
if (q%len==1) then
deallocate(q%fst)
q%lst=>null()
else
aux=>q%fst
q%fst=>q%fst%next
deallocate(aux)
end if
q%len=q%len - 1
end if
end subroutine leave

function first(q) result(y)


type(queue), intent(in) :: q
integer :: y

59
if (q%len > 0) then
y = q%fst%value
end if
end function first

function emptyQ(q) result(b)


type(queue), intent(in) :: q
logical :: b

if (q%len == 0) then
b = .true.
else
b = .false.
end if
end function emptyQ

recursive subroutine erase(q)


type(queue), intent(inout) :: q
type(node), pointer :: aux

if (q%len > 0) then


aux => q%fst
q%fst => q%fst%next
q%len = q%len - 1
deallocate(aux)
call erase(q)
end if
end subroutine erase

recursive function length(q) result(m)


type(queue), intent(in) :: q
integer :: m

m = q%len
end function length

subroutine show(q)
type(queue), intent(in) :: q
integer :: d,i
integer, dimension(q%len) :: auxv
type(node), pointer :: auxp

d = length(q)
auxp => q%fst
do i = d, 1,-1
auxv(i) = auxp%value
auxp => auxp%next
end do
print *, "|", auxv
end subroutine show
end module mqueues

Comecemos por analisar o tipo queue. Como já vimos, este é composto por dois apontadores para
node, em que o tipo node é igual ao utilizado na implementação dinâmica das pilhas. Por questões de
eficiência, acrescentou-se um terceiro campo (opcional), onde se guarda o comprimento da fila em cada
instante.

type, public :: queue


private
type(node), pointer :: fst
type(node), pointer :: lst

60
integer :: len
end type queue

A função new é semelhante à função new das pilhas. Neste caso, há que inicializar cada um dos
apontadores e colocar o comprimento a 0.

A função enter é um pouco mais complexa que a função push. Nomeadamente, há que considerar
várias condições.

subroutine enter(x,q)
integer, intent(in) :: x
type(queue), intent(inout) :: q
type(node), pointer :: aux

allocate(aux)
aux%value=x
if (q%len>0) then
q%lst%next=>aux
else
q%fst=>aux
end if
q%lst=>aux
q%len=q%len + 1
end subroutine enter

O primeiro passo consiste em criar um novo nó auxiliar, recorrendo à variável auxiliar aux, contendo no
campo value o valor x e desassociando o campo next (este vai ser o último elemento da fila e por isso
o campo next não aponta para nenhum nó):

allocate(aux)
aux%value = x

Em seguida, é necessário introduzir este elemento no fim da lista de nós encadeados. Há duas situações,
dependendo de a fila estar vazia ou não. Se a não estiver vazia, há que colocar o elemento que estava no
fim da fila a apontar para o elemento agora criado:

q%lst%next => aux

caso contrário, este é o primeiro elemento da fila e portanto, há que actualizar o campo fst:

q%fst => aux

Em qualquer dos casos, este será sempre o último elemento da fila, logo:

q%lst => aux

e é necessário actualizar também o comprimento da fila, atendendo a que esta tem mais um elemento:

q%len = q%len + 1

A função leave funciona da seguinte forma:

subroutine leave(q)
type(queue), intent(inout) :: q
type(node), pointer :: aux

if (q%len>0) then
if (q%len==1) then
deallocate(q%fst) ! also of q%lst
else

61
aux=>q%fst
q%fst=>q%fst%next
deallocate(aux)
end if
q%len=q%len - 1
end if
end subroutine leave

Se a fila não estiver vazia, há dois casos a considerar. Se tem só um elemento, há apenas que apagar o
(único) nó que existe e que é apontado simultaneamente por q%fst e q%lst (apaga-se fst e coloca-se
lst a apontar para null para evitar apontadores não associados). Caso exista mais do que um elemento, é
necessário guardar o primeiro nó numa variável auxiliar, para o apagar depois:

aux=>q%fst

em seguida, avança-se o apontador do primeiro elemento para o elemento seguinte:

q%fst=>q%fst%next

e, finalmente, apaga-se o primeiro elemento, apontado por aux:

deallocate(aux)

Deixa-se como exercício a análise das outras funções implementadas no módulo.

Vale a pena testar o funcionamento do módulo das filas, por exemplo com o programa seguinte:

program tqueues

use mqueues

type(queue) :: w

w = new()
if (emptyQ(w)) then
call show(w)
end if

call enter(111,w)
if (.not. emptyQ(w)) then
call show(w)
end if

call enter(222,w)
call enter(333,w)
call enter(444,w)
call show(w)

call leave(w)
if (.not. emptyQ(w)) then
call show(w)
end if

print *, "primeiro: ", first(w)

call erase(w)

end program tqueues

A execução deste programa resulta em:

62
|
| 11
| 44 33 22 11
| 44 33 22
primeiro: 22

A primeira linha corresponde a escrever a fila vazia no ecrã. A segunda linha corresponde à entrada do
elemento 11. Em seguida, entraram os elementos 22, 33 e 44, respectivamente, o que está patente na
terceira linha (note-se que o primeiro elemento é o elemento mais à direita). Em seguida, retirou-se o
primeiro elemento (11), o que é apresentado na quarta linha. Na última linha imprimiu-se o primeiro
elemento, ou seja 22.

Capítulo 5 Simulação discreta estocástica


Objectivos
Simulação discreta estocástica.

Tipos de simulação

Existem diversos tipos de simulação de sistemas:

 simulação analógica, realizada por modelos físicos, e simulação digital, realizada em


computador;
 simulação através de modelos contínuos e simulação através de modelos discretos;
 simulação determinista e simulação estocástica.

Neste capítulo, estudaremos uma técnica de simulação digital, discreta e estocástica. A técnica utilizada é
a do sequenciamento de eventos pendentes (SEP).

Descrição do problema

Pretende-se simular o tráfego num troço de estrada com portagem, só num dos sentidos.

Os veículos entram no referido troço com uma cadência aleatória no final do qual chegam à portagem
(apenas com um portageiro), ficam em fila até ao pagamento e, concluído este, saem do troço.

Pretende-se estudar a evolução do sistema (tempo médio de espera na portagem, tempo máximo de
espera, etc...) em função do tempo entre chegadas, do tempo de travessia e do tempo de pagamento.

Eventos e seus atributos

Segundo a técnica SEP, o primeiro passo consiste na identificação das categorias de eventos que afectam
o sistema a modelar. Neste caso, há que considerar três categorias (baptizadas segundo a tradição da
simulação de sistemas de filas de espera):

 "arr" (arrival): chegada de um veículo ao troço;


 "ess" (end of self-service): chegada de um veículo à portagem no final do troço;
 "dep" (departure): saída de um veículo da portagem após pagamento.

Cada evento é caracterizado por diversos atributos, entre os quais são essenciais:

63
 time: instante em que se dá o evento
 kind: categoria do evento

No caso presente, torna-se necessário considerar mais um atributo, car, que contém toda a informação
acerca do veículo associado ao evento.

O módulo seguinte estabelece de maneira trivial uma camada que disponibiliza o tipo car:

module mcar

public :: new_car, car_id, car_arr, car_ess, car_dep, &


set_sstime, set_deptime

type, public :: car


integer :: id
real :: ta
real :: ts
real :: td
end type car

contains

function new_car(i,a,s,d) result(c)


integer, intent(in) :: i
real, intent(in) :: a,s,d
type(car) :: c

c%id=i
c%ta=a
c%ts=s
c%td=d
end function new_car

function car_id (c) result(i)


type(car), intent(in) :: c
integer :: i

i=c%id
end function car_id

function car_arr (c) result(ta)


type(car), intent(in) :: c
real :: ta

ta=c%ta
end function car_arr

function car_ess (c) result(ts)


type(car), intent(in) :: c
real :: ts

ts=c%ts
end function car_ess

function car_dep (c) result(td)


type(car), intent(in) :: c
real :: td

td=c%td
end function car_dep

64
subroutine set_sstime (c,t)
type(car), intent(inout) :: c
real, intent(in) :: t

c%ts = t
end subroutine set_sstime

subroutine set_deptime (c,t)


type(car), intent(inout) :: c
real, intent(in) :: t

c%td = t
end subroutine set_deptime
end module mcar

Um carro é constituído por quatro componentes:


 id (inteiro): identificação do carro; assume-se que quando um carro entra no troço lhe é
atribuído um número de entrada, que o identifica univocamente (o primeiro carro recebe o
número 1, o segundo o número 2, etc);
 ta (real): instante de chegada do carro ao troço (arr);
 ts (real): instante de chegada do carro à portagem (ess);
 td (real): instante de saída do carro do troço após pagamento (departure).

As diversas componentes de cada carro vão sendo actualizadas à medida que os eventos correspondentes
vão ocorrendo. A função new_car serve para criar um novo objecto de tipo car. Recebe como
argumentos as características do carro (identificação e tempos) e devolve um objecto de tipo car. As
funções car_id, car_arr, car_ess, car_dep permitem consultar as características de um
carro (identificação e tempos). As funções set_sstime e set_deptime permitem alterar as
características do carro (neste caso, apenas vamos estar interessados em alterar os tempos de fim de self-
service e de saída do sistema).

Sobre esta camada, implementa-se uma camada que disponibiliza a construção de eventos e o acesso aos
respectivos atributos:

module mevent

use mcar

public :: evt, etime, ekind, ecar

type, public :: event


real :: time
character(len=3) :: kind
type(car) :: car
end type event

contains

function evt(t,k,c) result(e)


real, intent(in) :: t
character(len=3), intent(in) :: k
type(car), intent(in) :: c
type(event) :: e

e%time=t
e%kind=k
e%car=c
end function evt

function etime (e) result(t)

65
type(event), intent(in) :: e
real :: t

t=e%time
end function etime

function ekind (e) result(k)


type(event), intent(in) :: e
character(len=3) :: k

k=e%kind
end function ekind

function ecar (e) result(c)


type(event), intent(in) :: e
type(car) :: c

c=e%car
end function ecar
end module mevent

Repare-se que este módulo utiliza o módulo mcar. Num módulo, caso sejam utilizados outros módulos, é
necessário colocar a declaração private a seguir à declaração destes. As funções implementadas neste
módulo servem para construir eventos e obter as suas características e são em tudo semelhantes às
funções do módulo mcar.

Números pseudo-aleatórios

O segundo passo da técnica SEP é a definição das leis aleatórias que regem o acontecimento dos eventos.
Neste caso, há que considerar:

 chegadas: admite-se que o tempo entre chegadas consecutivas é uma variável aleatória com
distribuição exponencial de valor médio ba (between arrivals);
 travessias: admite-se que o tempo que cada veículo demora a atravessar o troço até à portagem é
uma variável aleatória com distribuição exponencial de valor médio ss (self-service);
 pagamentos: admite-se que o tempo que cada veículo demora a pagar a portagem (tempo entre o
início do pagamento, depois de ficar em primeiro lugar na fila, e a saída) é uma variável aleatória
com distribuição exponencial de valor médio st (service time).

Uma variável aleatória exponencial de valor médio m tem a seguinte distribuição:

t

1 e m

Por exemplo, fazendo t=6 e m=2, obtém-se o valor (aproximado) 0.95. A interpretação deste valor é a
seguinte: a probabilidade de que o valor observado seja inferior ou igual a 6 (três vezes o valor médio) é
aproximadamente 95%. Consequentemente, a probabilidade de que o valor observado seja três vezes
superior ao valor médio é de cerca de 5%.

O terceiro passo da técnica SEP consiste em definir procedimentos para simular a observação das
variáveis aleatórias em causa. Neste caso, o módulo seguinte define uma camada que disponibiliza a
subrotina exprandom.

module random_numbers

public :: exprandom

contains

66
subroutine exprandom(m,y)
real, intent(in) :: m
real, intent(out) :: y
real :: aux

call random_number(aux)
y=-m*log(aux)
end subroutine exprandom
end module random_numbers

Seria de esperar que exprandom fosse uma função. No entanto, a chamada à subrotina
random_number obriga a que exprandom seja também uma subrotina, neste caso com um argumento
de entrada, o valor médio m, e um argumento de saída, a observação da variável aleatória y. A justificação
do código utilizado na implementação será objecto de estudo em disciplina ulterior.

Podemos testar o módulo anterior através do programa seguinte:

program randteste

use random_numbers

real :: x
integer :: i

call random_seed()
do i=1,10
call exprandom(2.0,x)
print *,x
end do

end program randteste

Neste programa, calculam-se 10 observações de uma variável aleatória exponencial com valor médio 2. O
resultado obtido foi:

3.9982681
1.1937480
0.5594420
13.5625629
5.2368760
1.3448490
1.5555524
1.0824035
1.4392118
5.3243613

Cadeia de acontecimentos pendentes

Segundo a técnica SEP, o núcleo de um simulador é a cadeia de acontecimentos pendentes: CAP.

Para se entender a utilidade da CAP, considere-se o problema de simulação que está a ser analisado numa
situação intermédia, em que já há veículos no troço, veículos na fila de pagamento e está um veículo a
pagar. Como deverá continuar a simulação a partir desta situação?

Como se verá à frente, o simulador é composto por um ciclo em que em cada passo se simula o
acontecimento de um evento. Nestas condições põe-se a questão: qual é o próximo evento a simular?

Pode ser:

67
 a chegada de um novo veículo ao troço ("arr");
 o fim da travessia por parte de um veículo que já se encontra no troço, e consequente
chegada à portagem ("ess");
 a conclusão do pagamento por parte de um veículo que está a pagar ("dep").

A resposta é simples: vai-se buscar à CAP o próximo evento. Por próximo entenda-se o evento com
menor tempo (menor valor do atributo time) daqueles que ainda estão por simular.

Surge então outra questão: como alimentar a CAP de novos eventos?

A resposta a esta pergunta já não é tão simples. Genericamente, os novos eventos devem ser inseridos na
CAP aquando da realização de cada evento, tendo em atenção que na CAP devem estar os eventos que já
sabemos que terão que ser simulados no futuro mas que ainda não foram simulados por ainda não ter
chegado a vez deles.

 De cada vez que se simula uma chegada ao troço ("arr") deve-se colocar na CAP o evento
correspondente à chegada do próximo carro (um evento com atributo time igual a
ct+exprandom(ba), em que ct denota o instante actual (current time), e com atributo kind
igual arr), bem como um evento correspondente ao fim da travessia pelo veículo cuja entrada
se está a simular (um evento com atributo time igual a ct+exprandom(ss) e com atributo
kind igual a ess).
 De cada vez que se simula um fim de travessia do troço ("ess") há que verificar se o portageiro
está ocupado. Se estiver ocupado, há que colocar o veículo na fila de espera. Caso contrário,
começa a fase de pagamento: o portageiro fica ocupado e deve colocar-se na CAP um evento
relativo ao fim de pagamento (um evento com atributo time igual a ct+exprandom(st) e
com atributo kind igual a dep).
 Finalmente, de cada vez que se simula um fim de pagamento ("dep") há que examinar a fila de
espera. Se esta estiver vazia, há que colocar o portageiro no estado de desocupado. Caso
contrário, há que ir buscar o primeiro veículo à fila de espera, que inicia imediatamente o seu
pagamento, guardando-se na CAP um evento correspondente ao fim do pagamento (um evento
com atributo time igual a ct+exprandom(st) e com atributo kind igual a dep).

A CAP é portanto uma colecção de eventos com as operações disponibilizadas no seguinte módulo:

module mcap

use mevent

public :: newc, nextc, addc, deletec, emptycQ

type, public :: cap


private
type(node), pointer :: fst
end type cap

type, private :: node


type(event) :: evt
type(node), pointer :: next
end type node

contains

function newc() result(c)


type(cap) :: c

c%fst=>null()
end function newc

function nextc(c) result(e)

68
type(cap), intent(in) :: c
type(event) :: e

if (associated(c%fst)) then
e=c%fst%evt
end if
end function nextc

recursive subroutine insert(p,n)


type(node), pointer :: p
type(node), pointer :: n

if(.not. associated(p)) then


p => n
else if (etime(n%evt) < etime(p%evt)) then
n%next => p
p => n
else
call insert(p%next,n)
end if
end subroutine insert

subroutine addc(e,c)
type(event), intent(in) :: e
type(cap), intent(inout) :: c
type(node), pointer :: aux

allocate(aux)
aux%evt = e
aux%next=>null()
call insert(c%fst,aux)
end subroutine addc

subroutine deletec(c)
type(cap), intent(inout) :: c
type(node), pointer :: aux

if (associated(c%fst)) then
aux => c%fst
c%fst => c%fst%next
deallocate(aux)
end if
end subroutine deletec

function emptycQ(c) result(b)


type(cap), intent(in) :: c
logical :: b
if (associated(c%fst)) then
b = .false.
else
b = .true.
end if
end function emptycQ
end module mcap

A CAP consiste numa lista encadeada de nós, sendo cada nó constituído por um evento e um apontador
para o nó seguinte. A implementação escolhida mantém os eventos ordenados segundo o atributo time.
Descrevem-se em seguida as operações sobre a CAP.

A função newc devolve a CAP vazia:

69
function newc() result(c)
type(cap) :: c

c%fst=>null()
end function newc

Neste caso, há apenas que colocar o campo fst a apontar para null. A função nextc calcula o
próximo evento, ou seja, o evento no primeiro nó da CAP (caso esta não esteja vazia).

function nextc(c) result(e)


type(cap), intent(in) :: c
type(event) :: e

if (associated(c%fst)) then
e=c%fst%evt
end if
end function nextc

A função addc insere um evento na CAP. Como se disse atrás, a CAP é mantida ordenada pelo tempo
dos eventos. Assim, ao inserir um novo evento na CAP, há que procurar a sua posição, ou seja, é
necessário encontrar na CAP o primeiro evento que tenha tempo superior ao que se pretende inserir e
inserir este evento imediatamente antes. Esta função recorre à função auxiliar recursiva insert, para
inserir o evento na CAP.

subroutine addc(e,c)
type(event), intent(in) :: e
type(cap), intent(inout) :: c
type(node), pointer :: aux

allocate(aux)
aux%evt = e
aux%next=>null()
call insert(c%fst,aux)
end subroutine addc

recursive subroutine insert(p,n)


type(node), pointer :: p
type(node), pointer :: n

if(.not. associated(p)) then


p => n
else if (etime(n%evt) < etime(p%evt)) then
n%next => p
p => n
else
call insert(p%next,n)
end if
end subroutine insert

A função deletec apaga o primeiro evento da CAP (caso esta não esteja vazia).

subroutine deletec(c)
type(cap), intent(inout) :: c
type(node), pointer :: aux

if (associated(c%fst)) then
aux => c%fst
c%fst => c%fst%next
deallocate(aux)
end if

70
end subroutine deletec

A função emptycQ testa se a CAP está vazia, ou seja, se está a apontar a apontar para null.

function emptycQ(c) result(b)


type(cap), intent(in) :: c
logical :: b
if (associated(c%fst)) then
b = .false.
else
b = .true.
end if
end function emptycQ

Operações de medida associadas à simulação de cada evento

Aquando da simulação de cada evento há ainda que efectuar as operações de medida necessárias
correspondentes aos resultados de simulação pretendidos. No caso deste exemplo, estes resultados são:

 tempo médio de espera na fila da portagem;


 tempo máximo de espera na fila da portagem;
 tempo médio no troço de estrada;
 tempo mínimo no troço de estrada.

Outras medidas poderiam ter sido consideradas, como por exemplo, comprimento máximo da fila;
evolução do comprimento da fila ao logo do período de simulação, etc.

Como foi referido, cada evento tem um atributo car onde é guardado um objecto de tipo car, que, como
já se viu, inclui informação acerca da identificação do carro, bem como informação acerca dos instantes
em que ocorrem os eventos associados a este veículo.

Para cada veículo, os seus tempos devem ser actualizados aquando da ocorrência de eventos relevantes
para esse veículo. Por exemplo, o tempo de chegada (ta) deve ser actualizado assim que o veículo entra
no troço em estudo. Logo neste instante, é conhecido também o seu instante de saída (ts). Podemos optar
por actualizar logo o campo correspondente ou actualizá-lo apenas quando se dá a saída do sistema. O
tempo de saída do sistema pode ser actualizado quando o veículo começa a ser atendido pelo portageiro
(pelo que se disse atrás, isto pode não acontecer logo que o veículo chega à portagem) ou quando o
veículo termina o pagamento.

Quando o veículo acaba de pagar a portagem, são efectuadas as operações de medida, através da
observação dos tempos do veículo em causa.

Filas de espera revisitadas

No capítulo anterior foi apresentada uma implementação de filas de espera de números inteiros. No caso
do simulador, e pelo que já foi dito, é necessária uma estrutura semelhante a essa (ou seja com uma
disciplina FIFO) mas que, em vez de guardar números inteiros, guarda "veículos" (objectos de tipo car).

O módulo seguinte disponibiliza a camada correspondente:

module mqueue

use mcar

public :: newq, enterq, leaveq, firstq, emptyqQ, lengthq

type, public :: queue

71
private
type(node), pointer :: fst
type(node), pointer :: lst
integer :: len
end type queue

type, private :: node


type(car) :: car
type(node), pointer :: next
end type node

contains

function newq() result(q)


type(queue) :: q

q%fst=>null()
q%lst=>null()
q%len = 0
end function newq

subroutine enterq(c,q)
type(car), intent(in) :: c
type(queue), intent(inout) :: q
type(node), pointer :: aux

allocate(aux)
aux%car = c
aux%next=>null()
if (q%len > 0) then
q%lst%next => aux
else
q%fst => aux
end if
q%lst => aux
q%len = q%len + 1
end subroutine enterq

subroutine leaveq(q)
type(queue), intent(inout) :: q
type(node), pointer :: aux

if (q%len > 0) then


if (q%len == 1) then
deallocate(q%fst)
q%lst=>null()
else
aux => q%fst
q%fst => q%fst%next
deallocate(aux)
end if
q%len = q%len - 1
end if
end subroutine leaveq

function firstq(q) result(c)


type(queue), intent(in) :: q
type(car) :: c

if (q%len > 0) then


c = q%fst%car

72
end if
end function firstq

function emptyqQ(q) result(b)


type(queue), intent(in) :: q
logical :: b

if (q%len == 0) then
b = .true.
else
b = .false.
end if
end function emptyqQ

function lengthq(q) result(m)


type(queue), intent(in) :: q
integer :: m

m = q%len
end function lengthq
end module mqueue

Repare-se que este módulo é em tudo semelhante ao apresentado no capítulo anterior.

Simulador

Recorrendo às camadas anteriores, cuja hierarquia é descrita pela figura seguinte, é possível codificar o
simulador.

mqueue

mcap

mevent

mcar

random_numbers

Linguagem F

Note-se que algumas das camadas anteriores são independentes (por exemplo mqueue e cap), pelo a
figura anterior poderia ter outro aspecto, com a camada mcap acima da camada mqueue.

A estrutura genérica do simulador já foi descrita anteriormente. Consiste basicamente num ciclo em que
em cada passo se simula o próximo evento da CAP. Na simulação de cada evento, a CAP, a fila de espera
e as restantes variáveis de estado (estado do portageiro, instante actual, etc.), assim como as variáveis em
estudo (tempo médio de espera para pagar a portagem, etc.), são actualizadas.

As variáveis de estado são as seguintes:

 sch: CAP (schedule);


 qwc: fila de veículos à espera para pagar a portagem;
 tba: tempo médio entre chegadas;
 tss: tempo médio que um veículo demora a perrcorrer o troço de estrada em estudo;
 tse: tempo médio que um veículo demora a pagar a portagem;
 nc: número de veículos que já entraram no troço; serve como identificador de cada veículo;
 ct: instante actual;
 ce: evento em simulação;

73
 ck: categoria do evento em simulação;
 ht: tempo limite de simulação;
 busy: indica se o portageiro está ou não ocupado.

As variáveis em estudo são as seguintes:

 aat: tempo médio de espera (average awaiting time);


 mwt: tempo máximo de espera (maximum awaiting time);
 asst: tempo médio de permanência no troço da estrada, até chegar à portagem (average self-
service time);
 msst: tempo mínimo de permanência no troço da estrada, até chegar à portagem (minimum self-
service time);
 naat: contador, necessário para calcular o tempo médio de espera;
 nasst: contador, necessário para calcular o tempo médio de permanência no troço de estrada
até à portagem.

program simulador

use random_numbers
use mcar
use mevent
use mqueue
use mcap

type(cap) :: sch
type(queue) :: qwc
type(event) :: ce
real :: tba, tss, tse, ct, ht, awt, mwt, asst, msst
integer :: nc, naat, nasst
logical :: busy
character(len=3) :: ck

sch = newc()
qwc = newq()
call random_seed()
print *, "Tempo medio entre chegadas:"
read *, tba
print *, "Tempo medio de permanencia no troco:"
read *, tss
print *, "Tempo medio de pagamento:"
read *, tse
print *, "Tempo limite de simulacao:"
read *, ht
awt = 0
mwt = 0
asst = 0
msst = 999999.0

naat = 0
nasst = 0
busy = .false.

nc = 1
call exprandom(tba,ct)
ck = "arr"
ce = evt(ct,ck,new_car(nc,ct,0.0,0.0))

do
if (ct > ht) then

74
exit
end if
if (ck == "arr") then
call simArr()
else if (ck == "ess") then
call simEss()
else
call updatestats(ecar(ce))
call simDep()
end if
ce = nextc(sch)
ct = etime(ce)
ck = ekind(ce)
call deletec(sch)
end do
print *,"Tempo medio de espera na fila: ", awt/naat
print *,"Tempo medio no troco de estrada: ", asst/nasst
print *,"Tempo maximo de espera na fila: ", mwt
print *,"Tempo minimo no troco de estrada: ", msst

contains

subroutine simArr()
type(event) :: auxevt
type(car) :: auxcar
real :: auxtime

nc = nc+1
call exprandom(tba, auxtime)
auxtime = ct + auxtime
auxcar = new_car(nc,auxtime,0.0,0.0)
auxevt = evt(auxtime,"arr",auxcar)
call addc(auxevt,sch)
call exprandom(tss,auxtime)
auxtime = ct + auxtime
auxcar = ecar(ce)
call set_sstime(auxcar,auxtime)
auxevt = evt(auxtime,"ess",auxcar)
call addc(auxevt,sch)
end subroutine simArr

subroutine simEss()
type(car) :: cbs
type(event) :: auxevt
real :: auxtime

if (busy) then
call enterq(ecar(ce),qwc)
else
call exprandom(tse,auxtime)
auxtime = ct + auxtime
cbs = ecar(ce)
call set_deptime(cbs,auxtime)
auxevt = evt(auxtime,"dep",cbs)
call addc(auxevt,sch)
busy = .true.
end if
end subroutine simEss

subroutine simDep()

75
type(car) :: cbs
type(event) :: auxevt
real :: auxtime

if (lengthq(qwc) == 0) then
busy = .false.
else
cbs = firstq(qwc)
call leaveq(qwc)
call exprandom(tse,auxtime)
auxtime = ct+auxtime
call set_deptime(cbs,auxtime)
auxevt = evt(auxtime,"dep",cbs)
call addc(auxevt,sch)
end if
end subroutine simDep

subroutine updatestats(cbs)
type(car), intent(in) :: cbs

awt = awt+(car_dep(cbs)-car_ess(cbs))
naat = naat+1
if (mwt < car_dep(cbs)-car_ess(cbs)) then
mwt = car_dep(cbs)-car_ess(cbs)
end if
asst = asst+(car_ess(cbs)-car_arr(cbs))
nasst = nasst+1
if (msst > car_ess(cbs)-car_arr(cbs)) then
msst = car_ess(cbs)-car_arr(cbs)
end if
end subroutine updatestats
end program simulador

Exemplos

Ao executar o programa anterior, são solicitados 4 dados: tempo médio entre chegadas (tba), tempo
médio de permanência no troco (tss), tempo médio de pagamento (tse), tempo limite de simulação
(ht). Vamos considerar os valores 1.0, 30.0, 0.9 e 1000, respectivamente. Os resultados obtidos
para estes valores são:

Tempo medio de espera na fila: 6.0403237


Tempo medio no troco de estrada: 29.8328934
Tempo maximo de espera na fila: 24.9858398
Tempo minimo no troco de estrada: 1.3488770E-02

Repetindo a simulação para os mesmo dados, obtemos:

Tempo medio de espera na fila: 5.8854399


Tempo medio no troco de estrada: 31.4056683
Tempo maximo de espera na fila: 18.8579102
Tempo minimo no troco de estrada: 1.7089844E-03

Considerando agora tempos diferentes (tba=1.0, tss=30.0, tse=1.0, e ht=1000), obtemos:

Tempo medio de espera na fila: 41.4245720


Tempo medio no troco de estrada: 27.5069218
Tempo maximo de espera na fila: 72.7125854
Tempo minimo no troco de estrada: 6.9580078E-03

Deixa-se como exercício acrescentar ao simulador uma nova observação que permita contar o número de
veículos que excedem o limite de velocidade permitido por lei (comece por acrescentar um novo

76
parâmetro de entrada, o comprimento da estrada, e assuma que que as unidades de tempo são minutos).
Talvez não seja má ideia dar uma tolerância de 10% aos infractores.

Capítulo 6 Apoio ao projecto de Biocomputação

Enunciado 08/09
Problema

Desenvolva em F um programa para encontrar o máximo de uma função f: R+R+R+ num rectângulo
[x1,y1]  [x2,y2] de acordo com princípios de programação evolutiva. A ideia é constituir uma população
inicial de N indivíduos (pontos em [x1,y1]  [x2,y2]) dispostos aleatoriamente no rectângulo de pesquisa e
fazê-la evoluir até ao instante final , como se refere de seguida. Cada indivíduo z na posição (x,y) evolui
de acordo com conforto

(z)=1-e-f(x,y)

através dos mecanismos aleatórios seguintes:

 Morte, com tempo médio (-log(1- (z)))  ((z))(1/2) entre eventos.


 Reprodução, com tempo médio (-log( (z)))\ ((z))(1/2) de reprodução. A reprodução é
realizada escolhendo o indivíduo com melhor conforto de entre os k indivíduos mais próximos.
Da reprodução surge um terceiro indivíduo colocado aleatoriamente no intervalo delimitado
pelas posições dos progenitores. Caso o tamanho da população seja inferior ao limite Nmax a
reprodução realiza-se sempre. Caso o tamanho da população, Npop, ultrapasse este limite a
reprodução realiza-se com probabilidade Nmax/Npop.
 Deslocamento, com tempo médio (-log(1- (z)))  ((z))(1/2) entre movimentos. A deslocação de
um indivíduo z na posição (x,y) ocorre simultaneamente em x e em y, sendo o deslocamento (em
cada uma das direcções) um valor aleatório uniforme no intervalo

[-(1- (z))2, (1- (z))2].

Programa

O programa deve:
 receber interactivamente o conjunto seguinte de dados:

o os limites do intervalo (x1,y1) e (x2,y2);


o instante final  (>0) da evolução;
o lista de parâmetros N, Nmax, , , , k, ;

 devolver o indivíduo z com melhor conforto, isto é, cuja posição corresponda ao maior valor da
função de entre todos os indivíduos da população;
 mostrar, quando solicitado, o resultado de observações da população, realizadas de /20 em /20
unidades de tempo. Cada observação deve incluir o instante actual, o número de eventos já
realizados, a dimensão da população, e o melhor indivíduo.

Desenvolva o programa seguindo o método de programação por camadas centrado nos dados.

1. Comece por identificar os objectos de trabalho, nomeadamente indivíduo e população.


2. Implemente estas camadas sobre a camada básica da linguagem F. Posteriormente serão
disponibilizadas algumas destas camadas (ver Apêndice B).
3. Desenvolva de seguida o programa abstracto pretendido sobre a camada que disponibiliza
estes objectos.
4. Integre o programa obtido em 3 com o módulo desenvolvido em 2.

77
5. Experimente o programa desenvolvido em 4 com diversos conjuntos de dados à sua escolha.
Em particular, considere o conjunto:
 ponto inicial: $(x_1,y_1)=(0,0)$; ponto final: $(x_2,y_2)=(1,1)$;
 função f=x2y2;
 $\tau=10$;
 parâmetros de evolução: N=10, Nmax=200, =5, =2, =1, k=10, =1.

Sugestões

Evolução

A evolução do sistema consiste na simulação da vida de cada indivíduo da população.

Em cada passo do ciclo de simulação há que determinar o indivíduo da população com o próximo evento
a realizar (que será o indivíduo com o evento cujo instante seja o menor de todos). Uma vez determinado
esse indivíduo, há que retirá-lo da população, simular o evento correspondente (morte, reprodução ou
deslocamento):

 se o evento for a morte, não há mais nada a fazer, uma vez que o indivíduo não volta para a
população;
 se o evento for a reprodução, há que actualizar o indivíduo que com um novo tempo de
reprodução; em seguida, há que determinar se a reprodução se vai realizar ou não. Se a
dimensão da população for menor do que Nmax então a reprodução realiza-se. Caso contrário,
gera-se um número aleatório entre 0 e 1 e compara-se com Nmax/Npop. Se o número obtido for
menor, a reprodução realiza-se, caso contrário, não se realiza. Caso a reprodução se realize. Há
que determinar o parceiro da reprodução (o indivíduo com maior conforto de entre os k mais
próximos) e criar um novo indivíduo, resultante desta reprodução; finalmente, há que inserir os
dois indivíduos (o que se reproduziu e o descendente) na população; note-se, haja ou não
reprodução, o indivíduo do qual se simulou o evento deve ser actualizado e inserido na
população, e que as características do parceiro de reprodução se mantêm inalteradas;
 se o evento for uma mutação, há que actualizar a nova posição do indivíduo (calculando os
deslocamentos em x e em y) e o novo tempo de mutação; depois volta-se a inserir o indivíduo na
população.

Este ciclo repete-se até se atingir o tempo limite .

Tipos de dados relevantes

Para o desenvolvimento do programa é necessário implementar os tipos de dados a seguir descritos.

 Indivíduos:o tipo de dados ind inclui as seguintes operações:

o cria(x,y,tm,tr,td,cf): função que recebe nos parâmetros x e y, tm, tr, td e


cf de tipo real, respectivamente, as coordenadas da posição, um tempo de morte, um
tempo de reprodução, um tempo de mutação e um conforto e devolve um indivíduo de
tipo ind um indivíduo com estas características;
o pos(z): função que recebe no parâmetro de entrada z de tipo ind um indivíduo e
devolve um vector de dimensão 2 de tipo real com as coordenadas da posição do
indivíduo;
o tm(z): função que recebe no parâmetro z de tipo ind um indivíduo e devolve o seu
tempo de morte (de tipo real);
o tr(z): função que recebe no parâmetro z de tipo ind um indivíduo e devolve o seu
tempo de reprodução (de tipo real);
o td(z): função que recebe no parâmetro z de tipo ind um indivíduo e devolve o seu
tempo de deslocamento (de tipo real);

78
o conf(z): função que recebe no parâmetro z de tipo ind um indivíduo e devolve o
seu conforto (de tipo real);
o mts(z): função que recebe no parâmetro z de tipo ind um indivíduo e devolve o
menor dos seus três tempos (morte, reprodução ou deslocamento);
o evt(z): função que recebe no parâmetro z de tipo ind um indivíduo e devolve o tipo
de evento que esse indivíduo vai realizar: M se for uma morte, R se for uma reprodução
e D se for uma mutação. Nota: o valor devolvido é de tipo char(len=1).

 Populações: o tipo de dados pop inclui as seguintes operações:

o nova(): função sem parâmetros que devolve a população vazia;


o insere(p,z): subrotina que recebe no parâmetro de entrada/saída p de tipo pop
uma população e no parâmetro de entrada z de tipo ind um indivíduo e devolve em p
a população com o novo indivíduo inserido;
o proximo(p,z): subrotina que recebe no parâmetro de entrada p de tipo pop uma
população e devolve no parâmetro de saída z de tipo ind o primeiro indivíduo da
população, isto é, o indivíduo com menor tempo;
o apaga(p,z): subrotina que recebe no parâmetro de entrada/saída p de tipo pop uma
população e no parâmetro de entrada z de tipo ind um indivíduo e devolve em p essa
população sem o indivíduo z;
o melhor(p,z): subrotina que recebe no parâmetro de entrada/saída p de tipo pop
uma população e devolve no parâmetro de saída z de tipo ind o indivíduo com maior
conforto;
o parceiro(p,zi,k,zo): subrotine que recebe no parâmetro entrada/saída p de tipo
pop uma população, no parâmetro de entrada zi de tipo ind um indivíduo e no
parâmetro de entrada k de tipo integer a dimensão da vizinhança e devolve no
parâmetro de saída zo de tipo ind o melhor dos k indivíduos mais próximos;
o com (p): função que recebe no parâmetro p de tipo pop uma população e devolve o
seu tamanho (número de indivíduos).

Enunciado 06/07
Introdução

Na construção de estradas em terreno montanhoso, o traçado da estrada tem de ter em conta o relevo do
terreno que atravessa. Por um lado, fazer aterros, viadutos e túneis é mais caro que construir ao nível do
terreno, mas em contrapartida quanto mais curto for o percurso mais barata fica a construção da estrada.
Para determinar o traçado da estrada é necessário ter estes dois factores em conta e tentar encontrar um
percurso que por um lado minimize as variações em altura dos terrenos que atravessa, mas que também
não faça desvios demasiadamente grandes.

Na figura da esquerda mostra-se o relevo dum terreno quadrado (a gradação de cor num ponto
corresponde à cota desse ponto). Pretende-se construir uma estrada que atravesse este terreno do canto
inferior esquerdo ao canto superior direito; por conveniência, convencionou-se tomar para unidade de
comprimento a medida do lado do terreno. A figura da direita mostra um exemplo dum traçado possível
para a estrada que representa um bom compromisso entre os dois factores acima descritos. Note que esta
estrada é mais comprida que uma linha recta, mas em compensação evita variações grandes de nível.

79
Problema
O objectivo deste projecto é, utilizando princípios de Programação Evolutiva, desenvolver em F um
programa para encontrar uma solução para o problema acima.

O relevo do terreno é dado por uma função Φ : [0,1]×[0,1]→ℜ. No exemplo da figura acima, Φ(x,y) =
(y−x/3(32x2−48x+19))2. A estrada é uma curva parametrizada por dois polinómios p,q : [0,1]→[0,1] de
grau máximo gm, ou seja, é o conjunto dos pontos {(p(t),q(t)) : t[0,1]}.

Para resolver o problema, começa-se por gerar no instante zero uma população de ν indivíduos, todos
representando um caminho em linha recta (ou seja, p(t) = q(t) = t), e fazê-la evoluir até ao instante final τ.
A cada indivíduo z está associado um conforto φ(z)=e−η∑i=1N|Φ[p(i/N),q(i/N)]−Φ[p(i−1/N),q(i−1/N)]|, que intuitivamente
é proporcional à soma das variações médias de declives ao longo de N troços do percurso.

Cada indivíduo z evolui de acordo com o seu conforto através dos mecanismos aleatórios seguintes.
 Morte, com tempo médio −log(1−φ(z))μ entre eventos.
 Reprodução, com tempo médio −log(φ(z))ρ entre eventos. A reprodução é sempre efectuada
com o melhor indivíduo da população. Da reprodução surge um novo indivíduo cujo caminho é
parametrizado por polinómios obtidos da seguinte forma: primeiro, toma-se para coeficiente de
xk a média aritmética dos coeficientes de xk nos polinómios correspondentes dos dois
progenitores; em seguida divide-se o polinómio obtido pelo seu valor no ponto 1.
 Mutação, com tempo médio −log(φ(z))δ entre eventos. Uma mutação consiste em alterar um
coeficiente (que não o do termo independente) ou em um ou em ambos os polinómios; o novo
coeficiente é um número aleatório no intervalo de amplitude 2log(φ(z))ω/1+gm centrado no valor
original do coeficiente. Cada polinómio assim obtido é de novo dividido pelo seu valor no ponto
1.

A evolução da população é determinada pela evolução dos seus indivíduos.

Programa

O programa a desenvolver deve resolver este problema para a função de relevo Φ acima definida. Para tal,
deve:
 receber interactivamente o conjunto seguinte de dados:
o grau máximo gm dos polinómios a utilizar;
o instante final τ(>0) da evolução;
o lista de parâmetros ν,N,η,μ,ρ,δ e ω;
 devolver o indivíduo z com maior conforto e os coeficientes dos polinómios que parametrizam o
seu caminho;
 mostrar, quando solicitado, o resultado de 20 observações da população igualmente espaçadas no
tempo. Cada observação deve incluir o instante actual, o número de eventos já realizados, a

80
dimensão da população, os coeficientes dos polinómios que parameterizam o caminho do melhor
indivíduo encontrado e o conforto deste indivíduo.

Desenvolva o programa seguindo o método de programação por camadas centrado nos dados.

1. Comece por identificar os objectos de trabalho, nomeadamente polinómio, indivíduo e


população.
2. Implemente a camada dos polinómios sobre a camada básica da linguagem F. As restantes
camadas serão disponibilizadas posteriormente (ver Apêndice B).
3. Desenvolva de seguida o programa abstracto pretendido sobre a camada que disponibiliza estes
objectos.
4. Integre o programa obtido em 3 com o módulo desenvolvido em 2.
5. Experimente o programa desenvolvido em 4 com diversos conjuntos de dados à sua escolha. Em
particular, considere o conjunto:
o grau máximo dos polinómios: gm=5;
o τ=4;
o parâmetros de evolução: ν=10, N=1000, η=3/4, μ=200, ρ=δ=1 e ω=10.
6. Altere a função de relevo considerada e experimente o programa.

Sugestões

Evolução

A evolução do sistema consiste na simulação da vida de cada indivíduo da população.

Em cada passo do ciclo de simulação há que determinar o indivíduo da população com o próximo evento
a realizar (que será o indivíduo com o evento cujo instante seja o menor de todos). Uma vez determinado
esse indivíduo, há que retirá-lo da população, simular o evento correspondente (morte, mutação ou
reprodução):

 se o evento for a morte, não há mais nada a fazer, uma vez que o indivíduo não volta para a
população;
 se o evento for a reprodução, há que actualizar o indivíduo que com um novo tempo de
reprodução; em seguida, há que determinar o parceiro da reprodução (o indivíduo com maior
conforto) e criar um novo indivíduo, resultante desta reprodução; finalmente, há que inserir os
dois indivíduos (o que se reproduziu e o descendente) na população; note-se que as
características do parceiro de reprodução se mantêm inalteradas;
 se o evento for uma mutação, há que actualizar o caminho do indivíduo e o novo tempo de
mutação; depois volta-se a inserir o indivíduo na população.

Este ciclo repete-se até se atingir o tempo limite .

Tipos de dados relevantes

Para o desenvolvimento do programa é necessário implementar os tipos de dados a seguir descritos.

 Polinómios: o tipo de dados pol inclui as seguintes operações:

o novo(g,p): subrotina que recebe no parâmetro de entrada g de tipo integer o grau


do polínómio a criar e devolve em p de tipo pol o polinómio de grau g em que todos
os coeficientes são 0;
o atr(p,i,x): subrotina que recebe no parâmetro de entrada/saída p de tipo pol um
polinómio e nos parâmetros de entrada i e x de tipo integer e real,
respectivamente, dois valores e altera o coeficiente do termo de ordem i de p para x;

81
o coef(p,i,x): subrotina que recebe no parâmetro de entrada/saída p de tipo pol um
polinómio e no parâmetro de entrada i de tipo integer um valor e devolve no
parâmetro de saída x de tipo real o coeficiente do termo de ordem i;
o val(p,x,r): subrotina que recebe no parâmetro de entrada/saída p de tipo pol um
polinómio e no parâmetro de entrada x de tipo real um valor e devolve no parâmetro
de saída r de tipo real o valor do polinómio p em x (se este valor estiver acima de 1,
a subrotina deve devolver 1; se o valor estiver abaixo de 0, a subrotina deve devolver
0);
o norm(p): subrotina que recebe no parâmetro de entrada/saída p de tipo pol um
polinómio e devolve nesse parâmetro o polinómio dividido pelo seu valor no ponto 1;
o conv(p,v): subrotina que recebe no parâmetro de entrada/saída p de tipo pol um
polinómio e devolve no parâmetro de saída v um vector de números reais de
comprimento gm+1 contendo na posição i+1 o coeficiente do termo de ordem i.

 Indivíduos:o tipo de dados ind inclui as seguintes operações:

o fazind(p,q,tm,tr,td,cf,z): subrotina que recebe nos parâmetros de entrada


p e q de tipo pol e tm, tr, td e cf de tipo real, respectivamente, dois polinómios,
um tempo de morte, um tempo de reprodução, um tempo de mutação e um conforto e
devolve no parâmetro de saída z de tipo ind um indivíduo com estas características;
o px(z,p): subrotina que recebe no parâmetro de entrada z de tipo ind um indivíduo e
devolve no parâmetro de saída p de tipo pol o polinómio que parameteriza
horizontalmente o caminho associado a esse indivíduo (p);
o py(z,q): subrotina que recebe no parâmetro de entrada z de tipo ind um indivíduo e
devolve no parâmetro de saída q de tipo pol o polinómio que parameteriza
verticalmente o caminho associado a esse indivíduo (q);
o tm(z): função que recebe no parâmetro z de tipo ind um indivíduo e devolve o seu
tempo de morte (de tipo real);
o tr(z): função que recebe no parâmetro z de tipo ind um indivíduo e devolve o seu
tempo de reprodução (de tipo real);
o td(z): função que recebe no parâmetro z de tipo ind um indivíduo e devolve o seu
tempo de mutação (de tipo real);
o conf(z): função que recebe no parâmetro z de tipo ind um indivíduo e devolve o
seu conforto (de tipo real);
o mts(z): função que recebe no parâmetro z de tipo ind um indivíduo e devolve o
menor dos seus três tempos (morte, reprodução ou mutação);
o evt(z): função que recebe no parâmetro z de tipo ind um indivíduo e devolve o tipo
de evento que esse indivíduo vai realizar: M se for uma morte, R se for uma reprodução
e D se for uma mutação. Nota: o valor devolvido é de tipo char(len=1).

 Populações:o tipo de dados pop inclui as seguintes operações:

o nova(): função sem parâmetros que devolve a população vazia;


o insere(p,z): subrotina que recebe no parâmetro de entrada/saída p de tipo pop
uma população e no parâmetro de entrada z de tipo ind um indivíduo e devolve em p
a população com o novo indivíduo inserido;
o proximo(p,z): subrotina que recebe no parâmetro de entrada p de tipo pop uma
população e devolve no parâmetro de saída z de tipo ind o primeiro indivíduo da
população, isto é, o indivíduo com menor tempo;
o apaga_p(p): subrotina que recebe no parâmetro de entrada/saída p de tipo pop uma
população e devolve em p essa população sem o primeiro indivíduo (proximo);
o melhor(p,z): subrotina que recebe no parâmetro de entrada/saída p de tipo pop
uma população e devolve no parâmetro de saída z de tipo ind o indivíduo com maior
conforto;
o vazia(p): função que recebe no parâmetro p de tipo pop uma população e devolve
.true. se essa população estiver vazia, .false. caso contrário;

82
o tam(p): função que recebe no parâmetro p de tipo pop uma população e devolve o
seu tamanho (número de indivíduos).

Enunciado 04/05
Desenvolva em F um programa para encontrar o máximo de uma função f num intervalo [x1,x2] de acordo
com princípios de programação evolutiva. A ideia é constituir uma população inicial de  indivíduos
dispostos aleatoriamente no intervalo de pesquisa e fazê-la evoluir até ao instante . Cada indivíduo na
posição x evolui de acordo com o conforto

(x)=e(f(x)-)/

através dos mecanismos aleatórios exponenciais seguintes.

 Morte, com tempo médio m=(/m)(x) de vida.

 Reprodução, com tempo médio r=(m/r)(x)-2(N/) entre reproduções, em que N é o número


de indivíduos. A reprodução é realizada escolhendo o indivíduo com maior conforto de entre os
3 indivíduos mais próximos. Da reprodução surge um terceiro indivíduo colocado aleatoriamente
na vizinhança dos progenitores.

 Deslocação, com tempo médio d=(m/d)(x) entre movimentos e valor aleatório uniforme no
intervalo

[max(x1,x-), min(x2,x+)]

onde =max(10-4, ((x2-x1)/2)(x) –12).

Concluída a evolução, procura-se um indivíduo que maximize o valor da função.

O programa deve:

 Receber interactivamente os valores de x1 e x2 que delimitam o intervalo [x1,x2] de pesquisa e os


parâmetros de evolução , , , , m, r, d. A definição da função f deve ser feita no programa.
 Devolver um par (x,f(x)) que maximiza a função.
 Mostrar, quando solicitado, o resultado da observação da população, realizadas de /20 em /20
unidades de tempo. Cada observação deve incluir o instante actual, o número de eventos já
realizados, dimensão da população e posição do melhor indivíduo.

Sugestões

Evolução

A evolução do sistema consiste na simulação da vida de cada indivíduo da população.

Em cada passo do ciclo de simulação há que determinar o indivíduo da população com o próximo evento
a realizar (que será o indivíduo com o evento cujo instante seja o menor de todos). Uma vez determinado
esse indivíduo, há que retirá-lo da população, simular o evento correspondente (morte, deslocamento ou
reprodução):

 se o evento for a morte, não há mais nada a fazer, uma vez que o indivíduo não volta para a
população;
 se o evento for a reprodução, há que actualizar o indivíduo que com um novo tempo de
reprodução; em seguida, há que determinar o parceiro da reprodução (que consiste em
determinar os três indivíduos mais próximos e seleccionar o que tem maior conforto), e criar um
novo indivíduo, resultante desta reprodução; finalmente, há que inserir os dois indivíduos (o que

83
se reproduziu e o descendente) na população; note-se que as características do parceiro de
reprodução se mantêm inalteradas;
 se o evento for um deslocamento, há que actualizar a nova posição do indivíduo e o novo tempo
de deslocamento e inserir o indivíduo na população.

Este ciclo repete-se até se atingir o tempo limite .

Tipos de dados relevantes

O tipo de dados central é o tipo das populações. Uma população é uma colecção de indivíduos, pelo que
também é necessário o tipo de dados indivíduo. Sugerem-se em seguida algumas operações:

morte
reprodução
deslocamento

fazind(x,tm,tr,td,cf): dados uma posição x, instante de morte tm, instante da próxima


reprodução tr, instante do próximo deslocamento td e conforto cf, devolve o respectivo indivíduo;
pos(z): posição do indivíduo z;
tm(z): instante de morte do indivíduo z;
tr(z): instante da próxima reprodução do indivíduo z;
td(z): instante do próximo deslocamento do indivíduo z;
cf(z): conforto do indivíduo z;
evt(z): tipo de evento que o indivívuo z vai realizar (morte, reprodução ou deslocamento);
tmin(z): menor dos três instantes do indivíduo z;

popvazia(): população vazia;


insere(z,p): insere o indivíduo z na população p;
proximo(p): calcula o próximo indíviduo da população, isto é, o indivíduo que vai realizar o próximo
evento;
retira(p): retira o próximo indivíduo da população p;
vizinho(x,p): devolve a posição do indivíduo com melhor conforto de entre os três indivíduos mais
próximos da posição x;
melhor(p): o melhor indivíduo da população p, isto é, o indivíduo com maior conforto.

Relatório
Desenvolva o programa seguindo o método de programação por camadas centrado nos dados.

1. Comece por identificar os objectos de trabalho, nomeadamente indivíduo, população e evento.


Identifique as funções e subrotinas relevantes.
2. Implemente esta camada sobre a camada básica da linguagem F.
3. Experimente as funções e subrotinas desenvolvidas em 2. Comece por constituir uma população
com os seguintes indivíduos (inseridos na população pela ordem apresentada)
 p: 0.5; tm: 2.5; tr: 2.0; td: 1.2
 p: 1.0; tm: 3.0; tr: 4.0; td: 1.3
 p: 0.3; tm: 4.5; tr: 2.6; td: 0.7
 p: 0.2; tm: 3.8; tr: 1.1; td: 2.1
 p: 0.8; tm: 3.9; tr: 1.7; td: 1.9
em que p corresponde à posição do indivíduo, tm ao instante de morte, tr ao instante da próxima
reprodução, e td ao instante do próximo deslocamento. Em seguida, calcule o indivíduo que vai
realizar o próximo evento e o respectivo evento. Apague esse indivíduo da população e volte a
calcular o próximo indivíduo e respectivo evento.
4. Desenvolva de seguida o programa abstracto pretendido sobre a camada que disponibiliza estes
objectos.
5. Integre o programa obtido em 4 com o módulo desenvolvido em 2.

84
6. Experimente programa desenvolvido em 4 com diversos conjuntos de dados à sua escolha. Em
particular, considere o conjunto:
 f(x)=sen(1/x)/x;
 x1=1/(10), x2=1/(2), =10, =200, =20, =10, m=e, r=3e, d=1.

Apêndice

Relativamente às variáveis aleatórias, recorra à subrotina da linguagem F random para implementar as


seguinte subrotinas:
 para simular uma variável aleatória uniforme no intervalo [a, b] use a subrotina

subroutine vunif(a,b,x)
real, intent(in) :: a,b
real, intent(out) :: x

call random_number(x)
x=a+(b-a)*x
end subroutine vunif

 para simular uma variável aleatória exponencial com valor médio m use a subrotina

subroutine vexp(m,x)
real, intent(in) :: m
real, intent(out) :: x

call random_number(x)
x=-m*log(x)
end subroutine vexp

Capítulo 7 Complementos de programação imperativa

Objectivos
Passagem de funções por parâmetro. Integração númerica. Solução de equações não lineares. Algoritmos
de ordenação: método da inserção, método da troca (bubble sort), método da selecção e método de Hoare
(quick-sort). Algoritmos de pesquisa: pesquisa binária.

Integração numérica
Primeira abordagem

Pretende-se definir uma função integrate que receba como argumentos uma função real de variável
real f, dois números reais a e b e um número inteiro n e calcule o integral de f no intervalo [a,b]
(dividido em n subintervalos), tal como foi descrito anteriormente. A declaração de um parâmetro de tipo
função é feita através da declaração da interface da função, onde se indicam quais os tipos dos parâmetros
e qual o tipo do resultado. No caso presente, pretende-se que o parâmtero f seja uma função com um
parâmetro real e devolvendo um número real, o que é obtido através da declaração seguinte:

interface
function f(x) result(y)
real, intent(in) :: x
real :: y
end function f

85
end interface

Uma solução possível para a função integrate, disponibilizada pelo módulo mnumeric.f95, é a
seguinte:

function integrate(f,a,b,n) result(y)


interface
function f(x) result(y)
real, intent(in) :: x
real :: y
end function f
end interface
real, intent(in) :: a,b
integer, intent(in) :: n
real :: y,d,x,s
integer :: i

d=(b-a)/n
s=0
x=a
do i=1,n
s=s+f(x)
x=x+d
end do
y=d*s
end function integrate

O funcionamento desta função já foi descrito anteriormente. A principal diferença reside na necessidade
de declaração dos parâmetros e das variáveis auxiliares e, em particular, na declaração do parâmetro f.
Repare-se que, uma vez declarado, este parâmetro pode ser usado como uma função real de variável real,
como acontece na expressão s=s+f(x).

O programa seguinte utiliza esta função para calcular o integral da função quad (definida pela expressão
analítica -x2-x+2), disponibilizada no módulo mquad.

program testintegrate

use mnumeric
use mquad

real :: a,b
integer :: n

read *,a
read *,b
read *,n
print *,"O integral da funcao -x^2-x+2"
print *,"no intervalo [",a,",",b,"]"
print *,"e: ",integrate(quad,a,b,n)

end program testintegrate

O programa solicita ao utilizador os extremos do intervalo bem como o número de subdivisões pretendido
e calcula o respectivo integral, de acordo com o método apresentado.

Seguem-se alguns exemplos de execução:

-2.0
2.0
100

86
O integral da funcao -x^2-x+2
no intervalo [ -2.0000000 , 2.0000000 ]
e: 2.7456055

-3.0
3.0
10000
O integral da funcao -x^2-x+2
no intervalo [ -3.0000000 , 3.0000000 ]
e: -5.9991608

Note-se que a função que vai ser passada como argumento (neste caso a função quad) tem que estar
definida num módulo diferente do da função que a vai receber como parâmetro (neste caso, a função
integrate). Com efeito, a função quad está definida no módulo mquad e a função integrate está
definida no módulo mnumeric.

A solução anterior, embora simples, não conduz aos melhores resultados. Existem algoritmos mais
eficientes para calcular o integral de uma função.

Método do trapézio

Neste método, aproxima-se linearmente a função em cada sub-intervalo, como se descreve em seguida:

Nestas condições, o valor do integral

 f ( x)dx
a

(recorrendo a n sub-intervalos como anteriormente) é aproximado por

n
f ( xi 1 )  f ( xi )
 (x
i 1
i  xi 1 )
2

Escolhendo os sub-intervalos todos com mesmo comprimento

ba
d
n

chega-se a

 f (a  (i  1).d )  f (a  i.d )
d
2 i 1

Uma solução possível para a função trapintegrate, disponibilizada pelo módulo mnumeric.f95,
é a seguinte:

function trapintegrate(f,a,b,n) result(r)


interface
function f(x) result(y)

87
real, intent(in) :: x
real :: y
end function f
end interface
real, intent(in) :: a,b
integer, intent(in) :: n
real :: r,d,x,y,s
integer :: i

d=(b-a)/n
s=0
x=a
y=a+d
do i=1,n
s=s+f(x)+f(y)
x=y
y=y+d
end do
r=(s*d)/2
end function trapintegrate

O programa seguinte permite comparar as duas soluções, usando a função f(x)=1/(1+x):

program tintegrate

use mfuncoes
use mnumeric

real :: a,b
integer :: n

read *,a
read *,b
read *,n
print *,"Metodo do rectangulo:", integrate(f1,a,b,n)
print *,"Metodo do trapezio:", trapintegrate(f1,a,b,n)

end program tintegrate

Seguem-se alguns exemplos de execução, em que se considera o intervalo [0 , 1.5]:

0
1.5
5
Metodo do rectangulo: 1.0125277
Metodo do trapezio: 0.9225277

0
1.5
10
Metodo do rectangulo: 0.9628617
Metodo do trapezio: 0.9178617

0
1.5
100
Metodo do rectangulo: 0.9208066
Metodo do trapezio: 0.9163064

0
1.5

88
1000
Metodo do rectangulo: 0.9167395
Metodo do trapezio: 0.9162913

Não é difícil observar que o método do trapézio converge mais rapidamente para a solução
(log(5/2)0.9162907319).

Existem outros métodos para calcular o integral de uma função, com uma convergência mais rápida.
Esses métodos serão objecto de estudo em disciplina ulterior.

Resolução de equações não lineares


Pretende-se obter raízes aproximadas para uma equação não linear

f(x)=0

Como é sabido, existe uma fórmula para determinar raízes de uma equação algébrica do terceiro e quarto
grau, mas em geral, para equações de grau superior, é necessário recorrer a métodos numéricos.

Os métodos numéricos apresentados pressupõem o conhecimento de uma aproximação inicial da solução


para poderem ser usados. Em certos casos, é necessário que esta aproximação seja muito próxima da
solução para que estes métodos convirjam. É portanto fundamental saber localizar raízes reais, para
posteriormente ser possível aplicar métodos numéricos.

Localização de raízes reais

Se a função f na equação f(x)=0 é contínua no intervalo [a,b] e se f(a).f(b)<0 então o teorema do valor
intermédio garante a existência de uma raiz real na equação f(x)=0 em ]a,b[. Se f' não mudar de sinal
nesse intervalo então essa raiz é única nesse intervalo.

O método da bissecção é o método mais simples para calcular raízes de uma equação não linear e baseia-
se no teorema do valor intermédio.

Seja f a função obtida da equação f(x)=0, uma função contínua em [a,b] e tal que f(a).f(b)<0. Então existe
uma raiz neste intervalo. Sejam a0=a, b0=b, I0=[a0,b0] e x0 o ponto médio do intervalo I0. Então umas das
três condições seguintes verifica-se:

1. f(a0).f(x0 )<0 e então existe uma raiz em [a0,x0];


2. f(a0).f(x0 )>0 e então existe uma raiz em [x0,b0];
3. f(a0).f(x0 )=0 e então x0 é uma raiz.

Suponha-se, sem perda de generalidade, que se verifica a condição 1. Nesse caso, a1=a0, b1=x0 e
I1=[a1,b1]. Repita-se o procedimento anterior, sendo x1 o ponto intermédio do intervalo I1. Uma das três
condições seguintes verifica-se:

1. f(a1).f(x1 )<0 e então existe uma raiz em [a1,x1];


2. f(a1).f(x1 )>0 e então existe uma raiz em [x1,b1];
3. f(a1).f(x1 )=0 e então x1 é uma raiz.

Desta forma, obtém-se um intervalo I2. Aplicando este procedimento sucessivamente, é calculada um
sequência de intervalos I0I1I2... tal que a raiz rIk, para todo o k. Note-se que a amplitude dos
intervalos é reduzida geometricamente com razão ½, logo a amplitude dos intervalos tende para 0. Para
determinar uma raiz com erro menor ou igual a  basta iterar o método até que a amplitude do intervalo Ik
seja menor do que 2. A função bissec, disponibilizada no módulo mnumeric, permite encontrar uma
raiz de uma função contínua f no intervalo [a,b] com erro e:

89
function bissec(f,a,b,e) result(r)
interface
function f(x) result(y)
real, intent(in) :: x
real :: y
end function f
end interface
real, intent(in) :: a,b,e
real :: r,x,y,d

x=a
y=b
d=(b-a)/2
r=x+d
do
if ((d<e).or.(f(r)==0)) then
exit
else
if (f(x)*f(r)<0) then
y=r
else
x=r
end if
d=d/2
r=x+d
end if
end do
end function bissec

O programa seguinte permite testar esta função. Pretende-se calcular uma raiz da equação e-x-sen(x)=0:

program troots

use mfuncoes
use mnumeric

real :: a,b,e

read *,a
read *,b
read *,e
print *,"Raiz:", bissec(f2,a,b,e)

end program troots

A função f2 definida abaixo, que implementa a função f(x)=e-x-sen(x), encontra-se definida no módulo
mfuncoes:

function f2(x) result(y)


real, intent(in) :: x
real :: y

y=exp(-x)-sin(x)
end function f2

Pretende-se encontrar uma raiz desta equação no intervalo [0.25,0.75].

Com um erro máximo de 0.0001, obtém-se a seguinte solução:

> troots
0.25

90
0.75
0.0001
Raiz: 0.5885620

Com um erro máximo de 0.000001, obtém-se a seguinte solução:

> troots
0.25
0.75
0.000001
Raiz: 0.5885324

Ordenação de vectores
Apresentam-se agora alguns algoritmos para ordenar vectores (in situ).

Método da inserção

Este método de ordenação consiste em assumir os elementos do vector ordenados até à posição k. Em
seguida há que ordenar o elemento na posição k+1, deslocando-o para a esquerda até ficar na posição que
lhe corresponde (ou seja, até encontrar um elemento nas posições 1 até k que seja menor do que ele). A
subrotina insertsort apresentada abaixo, disponibilizada pelo módulo msort, implementa este
método:

subroutine insertsort(v)
integer, dimension(:), intent(inout) :: v
integer :: m, k, x

if (size(v)>1) then
do m=1,size(v)-1
do k=m,1,-1
if (v(k)<=v(k+1)) then
exit
else
x=v(k)
v(k)=v(k+1)
v(k+1)=x
end if
end do
end do
end if
end subroutine insertsort

Em cada momento, os elementos até à posição m estão já ordenados entre si. Considera-se o elemento na
posição seguinte e vai-se trocando este elemento com os elementos que estão nas posições anteriores até
se encontrar um que seja menor. Quando esse elemento for encontrado, o processo de troca pára e o
elemento fica na posição correcta. Vale a pena considerar um exemplo para perceber como funciona o
algoritmo. Considere-se o vector v=(1,3,4,2), ordenado até à posição 3 (ou seja, m=3). Para que o
vector fique ordenado até à posição 4, há que inserir o elemento 2 na posição correcta. Então, k toma o
valor inicial 3. Em seguida compara-se o elemento na posição 3 (4) com o da posição 4 (2) e se for maior
há que proceder à troca, ou seja, v fica com o vector (1,3,2,4). Em seguida, passa-se para a posição
anterior, ou seja, considera-se o elemento na posição 2 (3) e compara-se com o elemento na posição 3 (2).
Novamente, há que proceder à troca de elementos, ficando v com o vector (1,2,3,4). Finalmente, há
que comparar o elemento na posição 1 (1) com o elemento na posição 2 (2). Neste caso, não há que
proceder a nenhuma troca, pelo que o ciclo termina (com exit).

Método da troca (bubble sort)

91
Este método consiste em descolar para a posição k o k-ésimo menor elemento do vector, trocando
elementos consecutivos que estejam fora de ordem. A subrotina bubblesort apresentada abaixo, e
disponibilizada pelo módulo msort, implementa este método:

subroutine bubblesort(v)
integer, dimension(:), intent(inout) :: v
integer :: m, k, x
do k=1,size(v)-1
do m=size(v),k+1,-1
if (v(m-1)>v(m)) then
x=v(m-1)
v(m-1)=v(m)
v(m)=x
end if
end do
end do
end subroutine bubblesort

Considere-se o vector v=(4,2,1,3). A variável k toma o valor inicial 1. Para este valor de k, executa-
se um ciclo em que a variável m percorre os valores entre 4 (size(v)) e 2 (k+1). Para m=4, compara-se
o valor de v na posição 3 (que é 1) com o valor na posição 4 (que é 3). Como a condição do if não se
verifica, não se faz nada. Em seguida, m=3 e compara-se o valor de v na posição 2 (que é 2) com o valor
de v na posição 3 (que é 1). Como a condição do if se verifica, trocam-se estes dois elementos, ficando
v=(4,1,2,3). Finalmente, para m=2, compara-se o valor de v na posição 1 (que é 4) com o valor de v
na posição 2 (que é 1). Como a condição do if se verifica, trocam-se estes dois elementos ficando
v=(1,4,2,3). Ou seja, após a execução do ciclo exterior, para k=1, o vector v contém na primeira
posição o menor elmento (1). Após a execução deste ciclo para k=2, o vector v contém na segunda
posição o segundo menor elemento (2). Com efeito, após a execução deste passo do ciclo, o conteúdo do
vector v é (1,2,4,3) (TPC). Finalmente, após a execução do ciclo para k=3, o vector v contém na
terceira posição o terceiro menor elemento e, por exclusão de partes, na quarta posição o quarto menor
elemento, ou seja, está ordenado.

Outras relações de ordem

Suponha-se agora que se pretende ordenar um vector, mas que a ordem pela qual se pretendem ordenar
os elementos não é a relação usual mas sim outra. Suponha-se, por exemplo, que se pretende ordenar um
vector em que primeiro surjam todos os números pares ordenados entre si e em seguida todos os números
ímpares também ordenados entre si. Por exemplo, o vector (3,2,4,5,2,5,2,9,4,3) ordenado por
esta relação de ordem resulta no vector (2,2,2,4,4,3,3,5,5,9).
O primeiro passo consiste em definir uma função que implemente esta relação de ordem. A função
seguinte devolve .true. se o primeiro elemento é um número par e o segundo ou é um número par
maior do que o primeiro ou é um número ímpar, ou então se o primeiro argumento for um número ímpar
e o segundo argumento for um número ímpar maior do que o primeiro (assume-se que já foi
implementada a função mod que calcula o resto da divisão inteira):

function pred(x,y) result(b)


integer, intent(in) :: x,y
logical :: b

if (mod(x,2)==0) then
if ((mod(y,2)==0).and. (x<y)) then
b=.true.
elseif (mod(y,2)==1) then
b=.true.
else
b=.false.
end if
elseif ((mod(y,2)==1).and. (x<y)) then
b=.true.

92
else
b=.false.
end if
end function pred

É com esta relação de ordem que pretendemos ordenar o vector. Então, basta alterar um dos algoritmos
anteriores usando a relação pred em vez da relação <. A função seguinte implementa o algoritmo
bubblesort para uma relação de ordem arbitrária, que é passada por parâmetro.

subroutine bubblesortF(v,f)
integer, dimension(:), intent(inout) :: v
interface
function f(x,y) result(b)
integer, intent(in) :: x,y
logical :: b
end function f
end interface
integer :: m, k, x
do k=1,size(v)
do m=size(v),k+1,-1
if (.not.f(v(m-1),v(m))) then
x=v(m-1)
v(m-1)=v(m)
v(m)=x
end if
end do
end do
end subroutine bubblesortF
O programa seguinte permite testar este algortimo para a relação de ordem pred (definida no módulo
mfuncoes):

program tsort

use msort
use mfuncoes

integer, dimension(10) :: v

v= (/ 3,2,4,5,2,5,2,9,4,3 /)
call bubblesortF(v,pred)
print *,"Vector ordenado:", v

end program tsort

O resultado é:

Vector ordenado: 2 2 2 4 4 3 3 5 5 9

Algortimos de pesquisa
A pesquisa de um elemento x num vector ordenado v pode ser optimizada.

93
Pesquisa simples
Na pesquisa simples, basta percorrer um vector até encontrar o elemento e devolver .true. ou até
encontrar um elemento maior do que aquele que se pesquisa e devolver .false.. Apresenta-se em
seguinda uma versão iterativa deste algortimo de pesquisa. Deixa-se como exercício a implementação
deste algoritmo recursivamente.

function pesqsimples(v,x) result(y)


integer, dimension(:), intent(in) :: v
integer, intent(in) :: x
logical :: y
integer :: i

y=.false.
do i=1,size(v)
if (x==v(i)) then
y=.true.
exit
elseif (v(i)>x) then
exit
end if
end do
end function pesqsimples

Pesquisa binária

Na pesquisa binária, a ideia consiste em pesquisar apenas um segmento do vector delimitado pelas
posições pi e pf. Inicialmente, pi=1 e pf=size(v), calcula-se o ponto médio (pm) do vector e
compara-se com x:
 se x=v(pm) então devolve-se .true.;
 se x<v(pm) então a pesquisa tem que se efectuar entre as posições 1 e pm-1, ou seja, pf=pm-
1;
 se x>v(pm) então a pesquisa tem que se efectuar entre as posições pm+1 e size(v), ou seja,
pi=pm+1.

Este processo repete-se até que pi>pf.

function pesqbinaria(v,x) result(y)


integer, dimension(:), intent(in) :: v
integer, intent(in) :: x
logical :: y
integer :: pi,pf,pm

pi=1
pf=size(v)
do
if (pi>pf) then
y=.false.
exit
else
pm=(pi+pf)/2
if (x==v(pm)) then
y=.true.
exit
elseif (x<v(pm)) then
pf=pm-1
else
pi=pm+1
end if
end if

94
end do
end function pesqbinaria

Formata¸c˜ao de I/O
Objectivos
Formata¸c˜ao de sa´ıda
A formata¸c˜ao de sa´ıda consiste numa lista de c´odigos, separados por v´ırgulas e delimitados por
parˆenteses. Existem cinco categorias para estes c´odigos: f, es, a, l, e i. Os c´odigos da categoria f
(floating point) utilizam-se para formatar a apresenta¸c˜ao de nu´meros reais em nota¸c˜ao de v´ırgula
flutuante. A sua estrutura ´e da forma fn.d em que n define o nu´mero total de s´ımbolos que ser˜ao usados
na escrita do nu´mero real (incluindo o ponto decimal e o eventual sinal -). Por exemplo, o c´odigo f6.2
indica que se pretende escrever um nu´mero real usando, no m´aximo, 6 s´ımbolos e com duas casas
decimais. Assim, o maior nu´mero que se pode escrever com este c´odigo ´e 999.99 e o menor ´e -99.99.
No caso de o nu´mero n˜ao necessitar de todos os s´ımbolos, as posi¸c˜oes mais `a esquerda s˜ao
ocupadas por espa¸cos em branco. No caso de o nu´mero necessitar de mais s´ımbolos aparecem no ecr˜a
asteriscos em vez do nu´mero. Os trˆes comandos seguintes
print *, sqrt(2.0) print "(f6.2)", sqrt(2.0) print "(f7.3)", sqrt(2.0) produzem o seguinte resultado no ecr˜a.
1.4142135 1.41 1.414
Os c´odigos da categoria es (engineer and science) utilizam-se para formatar a apresenta¸c˜ao de
nu´meros reais em nota¸c˜ao cient´ıfica. A estrutura deste c´odigo ´e da forma esn.d com o mesmo
significado que anteriormente. Por exemplo, o c´odigo es8.2 indica que se pretende escrever um nu´mero
real em nota¸c˜ao cient´ıfica usando, no m´aximo, 8 s´ımbolos e com duas casas decimais. Os trˆes
comandos seguintes
print *, sqrt(20000.0) print "(es8.2)", sqrt(20000.0) print "(es9.3)", sqrt(20000.0) produzem o seguinte
resultado no ecr˜a.
141.42136 1.41E+02 1.414E+02
1
Os c´odigos da categoria i (integer) utilizam-se para formatar a apresenta¸c˜ao de nu´meros inteiros. A
sua estrutura ´e da forma in em que n define o nu´mero total de s´ımbolos que ser˜ao usados na escrita do
nu´mero inteiro. Por exemplo, o c´odigo i4 indica que se pretende escrever um nu´mero inteiro usando, no
m´aximo, 4 s´ımbolos. Neste caso, o maior nu´mero inteiro que se pode escrever com este c´odigo ´e
9999 e o menor ´e -999. Por exemplo, os dois comandos seguintes
print *,123, 43, 12 print "(i4,i4,i4)",123,43,12
produzem o seguinte resultado no ecr˜a
123 43 12 123 43 12 Os c´odigos da categoria a (alphanumeric) utilizam-se para formatar a
apresenta¸c˜ao de sequˆencias de s´ımbolos, isto ´e, de tipo character. Por exemplo,
print "(a)","1+1=2" produz o seguinte resultado no ecr˜a.
1+1=2
Escrita em ficheiros ´ E poss´ıvel escrever informa¸c˜ao em ficheiros. N˜ao se pretende aqui descrever
exaustivamente todas as primi- tivas de escrita em ficheiros, mas apenas as instru¸c˜oes b´asicas. O
primeiro passo consiste em abrir o ficheiro para escrita. O comando open tem a seguinte sintaxe:
open(unit=ficheiro, file=nome ficheiro, status=modo, access=acesso, action=ac¸c˜ao)
em que: • ficheiro ´e um nu´mero inteiro (por exemplo 2) que referencia um ficheiro externo; • nome
ficheiro ´e nome f´ısico do ficheiro entre aspas (por exemplo, "result.txt"); • modo ´e uma string que
especifica o modo de abertura do ficheiro. Pode ser "new", "old", "replace" ou "scratch". O modo "old"
refere-se a um ficheiro que tem que existir; o modo "new" refere-se a um ficheiro que n˜ao pode existir
(vai ser criado); no modo "replace" se o ficheiro n˜ao existir, ´e criado, se existir ´e apagado e criado um
novo ficheiro vazio com o mesmo nome. Recomenda-se a utiliza¸c˜ao deste u´ltimo modo quando se
desconhece o estado do ficheiro. • acesso ´e uma string que especifica o tipo de acesso ao ficheiro. Pode
ser "direct" ou "sequential". Recomenda-se a utiliza¸c˜ao do modo "sequential". • ac¸c˜ao ´e uma string
que especifica o tipo de ac¸c˜ao a realizar: "read", "write" ou "readwrite". Por exemplo, suponha-se que se
pretende criar um novo ficheiro result.txt para escrever informa¸c˜ao. O comando ´e o seguinte
open(unit=2, file="result.txt", status="replace", access="sequential", action="write")
2
Uma vez aberto o ficheiro para escrita, pode escrever-se informa¸c˜ao atrav´es do comando write. A
sintaxe do comando ´e a seguinte:
write(unit=ficheiro, fmt=especifica¸c˜ao) dados
em que: • ficheiro ´e um nu´mero inteiro que referencia um ficheiro externo (j´a aberto); • especifica¸c˜ao
´e uma lista de c´odigos de formata¸c˜ao (como descritos na sec¸c˜ao anterior) a ser aplicada a dados; •

95
dados ´e a informa¸c˜ao que se pretende escrever. Por exemplo, ap´os ter aberto o ficheiro result.txt como
descrito acima, o comando
write(unit=2,fmt="(a,i2)") "1+1="1+1 escreve no referido ficheiro a seguinte informa¸c˜ao:
1+1= 2 No fim, ´e necess´ario fechar o ficheiro. Para tal, utiliza-se o comando close, cuja sintaxe ´e a
seguinte:
close(unit=ficheiro)
em que ficheiro tem o mesmo significado que acima. Por exemplo, para fechar o ficheiro result.txt usar-
se-ia o comando seguinte.
close(unit=2)
3

96

Você também pode gostar