Moduli Fortran
Sottoprogrammi (procedure), moduli
La possibilità di decomporre un problema complesso in
sottoproblemi più semplici è cruciale
per la scrittura di software complessi e, nei linguaggi di tipo
imperativo
come Fortran, C, Pascal, Basic, corrisponde alla
possibilità di definire dei sottoprogrammi ciascuno
dei quali implementi solo parte, o meglio un sottoalgoritmo,
dell'
algoritmo complessivo.
Oltre a consentire di riflettere nella struttura del codice
la struttura dell' algoritmo, i sottoprogrammi si rivelano utili
in connessione con altri punti:
- possibilità di mettere a punto indipendentemente dal
resto del programma parti del codice, aiutando eventuali processi di
divisione del lavoro tra più programmatori;
- maggior modularità del codice: facilità a
sostituire parti del codice con nuove implementazioni, senza dover
alterare tutto;
- riutilizzabilità di parti del codice in altri progetti. Un
caso estremo sono le cosiddette librerire di sottoprogrammi che fungono
da vere e prorie estensioni del linguaggio, mettendo a disposizione
nuove funzionalità..
Il Fortran, per ragioni storiche, ha diversi tipi di
sottoprogramma. In questo corso ci limiteremo a function e subroutine.
Inoltre,
il linguaggio permette di definire questi sottoprogrammi in modi
diversi (indipendenemente dal programma, collegati a questo da
interfacce esplicite, come sottoprogrammi interni, procedure di
modulo, singole istruzioni). Qui ci limiteremo alle seguenti tre
modalità
- dichiarazione ed utilizzo come entità completamente
indipendenti;
- utilizzo attraverso l' uso di interfacce
- procedure di modulo
L' utilizzo raccomandato è solo attraverso i moduli.
Tuttavia, poiché a volte ci si
trova a dover operare con sottoprogrammi già organizzati in
librerie pre-compilate, verranno anche discusse le prime due
modalità di utilizzo. Le procedure interne posono
risultare estremamente utili, a volte, ma ne ometteremo la
discussione
per ridurre la complessità del linguaggio, almeno
nella prima fase di utilizzo. Si rimanda a testi e manuali di
riferimento per maggiori approfondimenti (cfr. bibliografia in coda al
programma del corso).
Function e subroutine
Le differenze morfologiche tra function e subroutine sono riconducibili
alle diverse modalità di utilizzo dei due tipi di
sottoprogramma.
L'utilizzo di una subroutine avviene mediante un' istruzione
(call) in cui si chiede al sistema
di eseguire il codice corrispondente alla subroutine, dopo di che il
programma continua con l' istruzione seguente alla chiamata alla
subroutine. La subroutine può modificare il valore di quacuno
dei suoi argomenti e quindi ritornare al programma chiamante
nuovi valori ma può anche non avere argomenti e/o eseguire altri
compiti (ad esempio generare output).
La function rispecchia l' utilizzo matematico delle
funzioni: all' interno di qualsiasi espressione, può
comparire il riferimento ad una function e il nome stesso della
function ne rappresenta il valore, utilizzabile direttamente nell'
espressione in cui compare.
Esempi:
...
call aaa(x1,x2,x3)
...
call bbb
...
w= 2*fun(x,y)-7*x*y
...
t = L*(ranf()-0.5)
...
I nomi aaa e bbb corrispondono a subroutine
mentre fun e ranf sono
function. Le variabili nelle parentesi dopo il nome del
sottoprogramma costituiscono gli argomenti con cui i
sottoprogrammi sono chiamati. Sia le subroutine sia le
function possono avere zero o più argomenti. Nel
caso di function con zero argomenti (ranf,
negli esempi) è però obbligatorio
scrivere ugualmente la coppia di parentesi.
Nella dichiarazione di subroutine e function, gli argomenti
(chiamati anche parametri formali, all' interno dei sottoprogrammi) non
rappresentano direttamente nuove variabili ma vanno considerati
come riferimenti agli argomenti con cui verrà realmente
chiamato il sottoprogramma durante l' esecuzione. Una prima
conseguenza di questo fatto è che il nome dei parametri
formali non ha nessuna attinenza coi nomi delle variabili
usate come argomenti.
Ai parametri formali occorre assegnare un tipo dati ben preciso. Questo
si fa con usuali dichiarazioni di tipo dati. Però, nel caso dei
parametri formali (e solo in quello) è possibile assegnare
un attributo intent al tipo
dati. Intent può assumere solo i valori in, out, inout o anche, equivalentemente, in out). Il significato dell'
intent(in) è che i corrispondenti parametri formali vanno
intesi come "read-only", solo in entrata, non modifcabili all' interno
del
sottoprogramma. Un' eventuale assegnazione verrebbe segnalata come
errore dal compilatore. Intent(out) informa il compilatore che il
corrispondente parametro permette di trasmettere un valore calcolato
nel sottoprogramma al programma chiamante. Infine intent(inout)
(il default), permette di modificare il valore nel programma chiamante
di una variabile usata come argomento del sottoprogramma.
Esempi di dichiarazione di sottoprogrammi:
Subroutine:
subroutine aaa(x,y,z)
implicit none
real, intent(in) :: x,y
real, intent(out) :: z
z = x + 2*y
end subroutine aaa
Function:
function fun(x,y) result(r)
implicit none
real, intent(in) :: x,y
real :: r
r = x**2+y**2
end function fun
Da notare l' assenza di intent per il risultato della function r:
è una variabile locale
alla function e non un argomento.
Le variabili locali, quelle cioè che non figurano tra gli
argomenti, ma dichiarate nel sottoprogramma, sono visibili ed
accessibili solo all' interno del sottoprogramma. Pertanto possono
avere
lo stesso nome di variabili del programma chiamante senza che vi sia
alcuna interferenza.
Se tra gli argomenti con intent in
oppure inout appare una
stringa
(variabile di carattere di lunghezza data) la dichiarazione del
parametro formale dovrà essere:
character(len=*),intent(in) :: stringa
oppure
character(len=*),intent(inout) :: stringa
La lunghezza della variabile stringa non è indicata
esplicitamente (anche perché difficilmente nota in
anticipo) ma sarà uguale a quella dell' espressione di
tipo character con cui il sottoprogramma sarà chiamato.
All'interno del sottoprogramma, l' utilizzo della funzione di libreria len permetterà di
accedere alla lunghezza dell' argomento corrispondente: len(stringa).
Infine, se tra gli argomenti appare un array la dichiarazione del
parametro formale corrispondente può assumere diverse forme.
Nel seguito ci limiteremo alla sintassi corrispondente ai
cosiddetti assumed-shape
arrays o array di forma presunta.
Il loro utilizzo è però possibile solo nei programmi che dispongono
dell' interfaccia esplicita del sottoprogramma (vedi dopo), per esempio l'iterfaccia esplicita esiste in tutti i sottoprogrammi di modulo..
La sintassi corrisponde ad indicare nell' attributo dimension solo il
numero di indici (o al più il loro estremo inferiore) con
il carattere "due punti" (:). Es:
integer,dimension(:,:),intent(in) :: a
complex,dimension(:),intent(out) :: z
real,dimension(0:,-5:),intent(inout) :: w, p
L' estensione degli array di forma presunta si ricava attraverso la
function size. size(a,i)
fornisce il numero di componenti dell' array a relativi all'
i-esimo indice. P.es., in uma matrice, size(a,2)
ritornerà il numero di colonne della matrice (il secondo
indice è quello di colonna).
Nella chiamata ad una procedura gli argomenti con intent in possono essere costituiti da espressioni, ovvero
da
- variabili
- costanti (letterali o simboliche)
- espressioni costruite mediante operatori a valori nel tipo dati attribuito al corrispondente
parametro formale.
Moduli
Un modulo è assimilabile ad un contenitore software che
può avere al suo interno: costanti simboliche, variabili
globali,
definizioni di nuovi tipi dati e sottoprogrammi. Il contenuto di un
modulo diviene accessibile alle sole unità di programma
che usano quel modulo.
Le ragioni per preferire l' uso dei sottoprogrammi definiti
all'
interno dei moduli sono diverse:
- i moduli comportano la costruzione dell' interfaccia da
parte del compilatore, semplificando il compito del programmatore;
- i moduli permettono un migliore controllo sull'
accessibilità, eventualmente parziale, di
più sottoprogrammi tra loro collegati,
migliorando la modularità del codice;
- i moduli permettono di emulare gran parte delle
funzionalità delle classi del paradigma di programmazione
ad oggetti, in cui l' accento viene spostato dai
sottoprogrammi ai dati su cui questi agiscono. In effetti i
moduli permettono di relizzare un legame stretto, a livello del codice,
tra dati e sottoprogrammi ad essi collegati.
Schematicamente un modulo è costituito da un blocco di
istruzioni comprese tra le due istruzioni
module nome_modulo
...
end module nome_modulo
Dove nome_modulo
è un qualsiasi nome valido Fortran
(stringa di max 31 caratteri, solo alfanumerici e underscore
("_"), con l' obbligo di non iniziare con una cifra.
Nel modulo ci possono essere una o due sezioni. Se
c'è la seconda, il suo inizio viene segnalato dalla
parole CONTAINS.
Nella prima sezione vanno obbligatoriamente le dichiarazioni di
variabili e costanti del modulo, i tipi dati derivati
e le eventuali dichiarazioni di carattere pubblico o privato delle
entità definite nel modulo o la richiesta che i dati siano
conservati tra un utilizzo ed un altro del modulo stesso. Tutto
ciò che viene dichiarato privato ( PRIVATE ) nel modulo
non è visibile nelle procedure che utilizzano il modulo.
L' istruzione
PRIVATE
rende "private" l' attributo di default di ogni entita' del modulo.
E' consigliabile iniziare ogni modulo con l' istruzione
SAVE
in modo che il valore dei campi dati eventualmente definiti nel modulo
siano preservati anche nel caso di chiamate a sottoprogrammi che non
utilizzino il modulo.
Per utilizzare un modulo, ciascun sottoprogramma che ne ha
bisogno, dovrà avere un' istruzione
USE nome_modulo
Come prima istruzione subito dopo il nome del sottoprogramma stesso.
L' istruzione USE rende dunque visibili nella procedura che
utilizza il modulo tuti i nomi pubblici dello stesso. Eventualmente si
può utilizzare solo alcune delle entità
del modulo attraverso il meccanismo del parametro ONLY
nell' istruzione USE. Ad esempio, con
USE nome_modulo, ONLY:n1,n2,n3
si rendono visibili nel programma che usa il modulo solo le
entità (variabili, tipi dati, sottoprogrammi)
di nome n1,n2,n3, indipendentemente dall' esistenza di altre
entità pubbliche.
E' anche possibile che all' interno del programma che
utilizza il modulo, alcune entità del modulo siano
ribattezzate con un nome diverso. Per esempio, per usare un
modulo e far sì che la variabile del modulo di nome
var_mod sia indicata nel
programma come var_prog,
basta
usare l' istruzione nel seguente modo:
USE nome_modulo, var_prog => var_mod
Un modulo può usare un altro modulo (ma non è consentita
ricorsione).
Procedure esterne
L' uso di procedure non presenti all' interno di un modulo è potenzialmente pericolosa (non vengono effettuati controlli sulla corrispondenza tra numero e tipo degli argomenti tra programma chiamante e chiamato. Nel caso non sia possible farne a meno, può essere utile indicare nel programma chiamante questa situazione mediante la dichiarazione
EXTERNAL :: nome_procedura
nel caso di function che ritorni un tipo dati, il nome della function va anche dichiarato del tipo dati in questione. questione. P.es. se abbiamo una function che nome FX e calcola un valore real con kind rk la dichiarazione dovrà essere
REAL(kind=rk), EXTERNAL :: FX