A Fortran programozási nyelv

Objektum orientált programozás támogatása FORTRAN95-ben

Az objektum-orientált programozás egy tervezési filozófia komplex szoftverek írásához. A központi eszköze az absztrakt adattípus, ami lehetővé teszi a magasabb szinten való gondolkodást és programozást, mint pusztán a számok és számtömbök. A matematikusok és a fizikusok világában ismerős dolog az absztrakció ereje, például fizikai egyenletekben az összes komponenes kiírása helyett gyakran használnak külön erre bevezetett operátorokat. De ilyen jellegű absztrakciókat ritkán használunk programozás során.

Az OOP számos fogalmat tartalmaz, ami hasznosnak bizonyul nagy projektek implementálásában. Ezek:

Információrejtés és az adat beburkolása

Talán a legfontosabb fogalom mind közül az információrejtés. Ez azt jelenti, hogy egy olyan információ, amelyre csak egy eljárásban van szükség, azt nem szabad más olyan eljárások tudomására hozni, amiknek nincs szükségük erre az információra. Mint a CIA-ben, az eljárásokat csak azokról az adatokról kell informálni, amelyekre feltétlenül szükségük van. Ez a filozófia egyszerűsíti a programozást, mert kevesebb részlettel kell foglalkozni, és kisebb a hibalehetőség esélye is.

Az információrejtés filozófiájának egyik megvalósító eszköze, hogy az adatot egy származtatott típusba burkoljuk, és csak bizonyos eljárásokon keresztül (gyakran metódusoknak hívjuk őket) engedjük módosítani őket.

Ez a beburkolás lehetővé teszi a problémák szétválasztását. Elkülönülten lehet írni egy nagy program darabjait anélkül, hogy foglalkozni kellene egy új eljárás által egy régin okozott sérülésre. Komplex programok írása ezáltal egy N nagyságrendű problémává válik N*N probléma helyett. Nézzünk egy példát arra, hogy ez mit is jelent. Figyeljük meg a következő interfészt, ami egy Fortran77 örökség:

subroutine fft1r(f,t,isign,mixup,sct,indx,nx,nxh) integer isign, indx, nx, nxh mixup(nxh) real f(nx) complex sct(nxh), t(nxh) ... az eljárás többi része ide jön

Ebben az eljárásban f az adat, amin a műveletet kell végrehajtani, t egy ideiglenes munkatömb, mixup egy bit cserélő tábla, sct egy szinusz/koszinusz tábla, indx az átalakítás hosszát adja meg, nx az f mérete, nxh a megmaradó adat hossza és isign vagy a transzformáció iránya (-1 vagy 1) vagy a tábla inicializálására vonatkozó kérés (0).

Ahhoz, hogy használhassuk ezt az eljárást, az összes adatot helyesen át kell adni, és ez rengeteg hibalehetőséget rejt magában. Mindamellett a legtöbb paraméter csak a belső működési részletek megadásáért felelős. A programozó csak magával az adattal (f) és az iránnyal (isign) szeretne foglalkozni. Az élet sokkal egyszerűbb lenne, ha pusztán a következő módon lehetne hívni az eljárást:

call fft1(f,isign)

anélkül, hogy a többi részlettel kellene foglalkozni.

Az egyik oka annak, hogy mindezek a részletek nincsenek elfedve, az, hogy a Fortran77 nem támogatta dinamikus tömbök használatát. Automatikus tömbök használatával könnyedén el lehet rejteni az ideiglenes számoló tömböt (t), a mixup és a sct táblákat egy csomagoló függvénybe:

subroutine fft1(f,indx,isign,nx,nxh) integer indx, isign, nx, nxh real f(nx) complex, dimension(nxh) :: t integer, dimension(:), allocatable, save :: mixup complex, dimension(:), allocatable, save :: sct if (isign==0) allocate(mixup(nxh),sct(nxh)) call fft1r(f,t,isign,mixup,sct,indx,nx,nxh)

Itt a programozónak nem kell törődnie ezekkel a dolgokkal többé, és így kevesebb lehetősége van a hibázásra.

A Fortran95 tömbök magukba foglalják a dimenzióra vonatkozó információt is, ezáltal kivehetünk minden dimenzióra vonatkozó információt az interfészből:

subroutine fft1(f,indx,isign) integer :: indx, isign, nx, nxh real, dimension(:) :: f complex, dimension(size(f)/2) :: t integer, dimension(:), allocatable, save :: mixup complex, dimension(:), allocatable, save :: sct nx = size(f); nxh = nx/2 if (isign==0) allocate(mixup(nxh),sct(nxh)) call fft1r(f,t,isign,mixup,sct,indx,nx,nxh)

Sikeresen elrejtettük azokat a részleteket a programozó elől, amiket nem szükséges tudnia ahhoz, hogy használja az eljárást. Így az interfész sokkal egyszerűbb lett és kevesebb hibalehetőséget rejt magában:

call fft1(f,indx,isign)

Ha belegondolunk az interfész igazi jelentőségébe, akkor látható, hogy valószínűtlen, hogy megváltozik a jövőben, még akkor is, ha az eljárás belső részletei megváltoznak. Például tegyük fel, hogy adott egy számítógép, ahol egy optimalizált változata adott az eljárásnak, ami sokkal gyorsabb, mint az örökölt fft1r. Ekkor könnyen ki lehet cserélni az eljárás hívását a csomagoló függvényben, és a csomagolófüggvény használóinak semmit nem kell változtatniuk a kódjukon.

subroutine fft1(f,indx,isign) ... call faster_fft1r(f,......) ! Megváltozott belső argumentumok end subroutine call fft1(f,indx,isign) ! Vegyük észre, hogy a hívás nem változik

Ez a beburkolás lehetővé teszi az implementációs részletek változtatását anélkül, hogy a program többi részén változtatni kellene. Ez lehetővé teszi a konkurens programozást is: különböző programozók módosíthatják egy nagy program különböző részeit, anélkül, hogy egymás útjában állnának, mindaddig, amíg az interfészek nem változnak.

Tovább tökéletesíthetjük ezt az interfészt, ha észrevesszük, hogy az indx argumentum, ami az fft hosszát határozza meg, valamint a mixup és sct belső tábláknak egymással konzisztenseknek kell lenniük. A tábláknak akkor kell létrejönniük, ha az isign paraméter értéke 0:

call fft1r(f,indx,isign=0) ! táblák létrehozása

De ha az fft később hívódik meg, akkor az indx paraméter különbözhet.

call fft1(f,kndx,isign=1) ! rossz indx érték

A probléma egy része itt abból adódik, hogy az örökölt fft1r-t két teljesen különböző művelet végrehajtására használjuk: tábla inicializálásra és transzformációra. Jobb ha két különböző függvényünk van a két különböző műveletre.

De a táblák az csomagoló ff1 csomagoló függvény belsejében eltárolt privát tömbök. Hogyan tudja egy másik eljárás inicializálni ezt a táblát? Ennek megoldására több út létezik. Az egyik, hogy a táblákat egy modulba tesszük, ami a modul összes eljárása számára hozzáférhető:

module fft1 integer, save :: saved_indx integer, dimension(:), allocatable, save :: mixup complex, dimension(:), allocatable, save :: sct contains subroutine new_fft_table(indx) ! fft tábla létrehozása integer :: indx, isign, nx, nxh saved_indx = indx isign = 0 nx = 2**saved_indx; nxh = nx/2 allocate(mixup(nxh),sct(nxh)) call fft1r(f,t,isign,mixup,sct,indx,nx,nxh) end subroutine new_fft_table subroutine fft1(f,isign) ! fft végrehajtása integer :: indx, isign, nx, nxh real, dimension(:) :: f complex, dimension(size(f)/2) :: t nx = 2**saved_indx; nxh = nx/2 call fft1r(f,t,isign,mixup,sct,indx,nx,nxh) end subroutine fft1 end module fft1

Ezeket az eljárásokat a következő módon lehet használni:

use fft1 call new_fft_table(indx) ! új fft tábla létrehozása call fft1(f,isign=1) ! indx nincs többé az argumentumok között

Szükségünk van egy harmadik eljárásra is a modulban, ami felszabadítja a táblákat, ha nem hajtunk végre több fft-t.

subroutine delete_fft_table() ! fft táblák törlése deallocate(mixup,sct) end subroutine delete_fft_table

Egy másik lehetőség egy hozzáférésvezérlő hozzáadása. Ha a következő sorokat írjuk a modul elejére:

module fft1 private public: new_fft_table, delete_fft_table, fft1

akkor az fft táblákhoz nem lehet hozzáférni a modulon kívülről. A tábla manipulálásának egyetlen módja a new_fft_table és a delete_fft_table eljárások meghívása.

Gyakorlásképpen gondolkozzunk további hibaellenőrzéseken, amiket az eljárások belesejébe lehetne tenni, például hogyan lehetne megelőzni egy még nem létrehozott fft táblára vonatkozó fft hívást.

Ezzel az összes fft-khez tartozó műveletetet egyetlen modulba csoportosítottunk. Ilyenfajta csoportosítás szintén része az egységbezárás koncepciójának. Az információrejtés és az adatbeburkolás vitathatatlanul a leghasznosabb és legfontosabb fogalmak az objektum-orientált programozásban.

Függvénytúlterhelés vagy statikus polimorfizmus

A függvény túlterhelés egyazon eljárásnévvel különböző műveletek végrehajtását jelenti, ami az argumentumtípusok különbözőségén alapul. A Fortran77-ben már megvolt ez a lehetőség. Például a real() függvény különböző dolgokat jelentett a típustól függően.

integer :: i real :: a complex :: z a = real(i) ! integer-t kovertál real-lá a = real(z) ! a komplex z szám valós részét veszi

Fortran95-ben a generikus függvények ezt lehetővé teszik felhasználó által definiált függvények esetén is. Például több különböző típusú FFT léthet: valós->komplex, komplex->komplex, egydimenziós, kétdimenziós, egyszeres pontosságú, kétszeres pontosságú, stb. Fortran77-ben különböző neveket kellett adnunk minden FFT-nek. Amíg egyértelmű, hogy ezek mindegyike mit csinál, a Fortran95 megengedi, hogy ugyanazt a nevet használjuk mindegyikre, generikus interfészek használatával:

interface fft ! fft generikus név definiálása module procedure fft1rc module procedure fft1cc ... end interface

mindaddig, amíg a függvények mindegyike különböző argumentumtípusokkal rendelkezik.

subroutine fft1rc(f) real, dimension(:) :: f ! valós tömb argumentum subroutine fft1cc(f) complex, dimension(:) :: f ! komplex tömb argumentum subroutine fft2rc(f) real, dimension(:,:) :: f ! valós 2 dimenziós tömb argumentum

és így tovább.

Emellett könnyű túlzásba vinni a függvény túlterhelést. Csak olyan esetekben kell használni, amikor nyilvánvaló, hogy mit fejez ki. Kerülni kell a használatát olyan esetekben, ha az csak összezavarja a szándékunkat. Például különböző eljárásokat túlterhelni egy "megold" generikus nélvvel esetleg azt eredményezheti, hogy később nem fogunk emlékezni, hogy éppen melyik megoldásra gondoltunk, anélkül, hogy gondosan megtanultuk volna az összes argumentum típusát a modulban. Ezenkívül cél az is, hogy más ember is képes legyen elolvasni a kódot és könnyen megértse, hogy az mit akar csinálni.

A függvény túlterhelést gyakran statikus polimorfizmusnak nevezik, mert annak eldöntése, hogy éppen melyik függvényt kell hívni, fordítási időben történik, nem pedig futási időben.

Absztrakt adattípusok, osztályok és objektumok

Egy absztrakt adattípus vagy osztály egységbe zár egy felhasználó által definiált típust annak műveleteivel, amik a típuson hajtódnak végre. Például vegyünk egy osztályt, amit Alkalmazott-nak hívunk, és legyen a feladata adatbázis rekordok manipulálása. Az adat, amit tartalmaz: egy személy azonosítója és a neve. A függvények, amiket végre lehet hajtani rajtuk: egy új rekord létrehozása vagy egy meglévő törlése, egy rekord kinyomtatása és egy rekordból az azonosító kinyerése. Ez Fortran95 osztállyal megvalósítva így néz ki:

module Alkalmazott_class type Alkalmazott private integer :: ssn character*12 :: firstname, lastname end type Alkalmazott contains subroutine new_Alkalmazott(this,s,fn,ln) ... subroutine delete_Alkalmazott(this) ... subroutine print_Alkalmazott(this,printssn) ... function getssn_Alkalmazott(this) result(ssn) ... end module Alkalmazott_class

Ennek a típusnak egy változóját objektumnak nevezzük. Ezt a következőképpen lehet deklarálni:

type(Alkalmazott) :: szemely

A származtatott típus komponenseit az osztály adattagjainak hívjuk. Ezek gyakran private-ként vannak deklarálva, azaz ezek a komponensek nem hozzáférhetőek az osztályon kívülről. Az osztályban definiált eljárásokat tagfüggvényeknek hívjuk. Általában csak ezeken keresztül lehet az Alkalmazott objektumokat manipulálni. Egy függvény, amit mindig szükséges definiálni, a konstruktor, ami a rekord inicializálására szolgál. Például:

subroutine new_Alkalmazott(this,s,fn,ln) ! Konstruktor type (Alkalmazott), intent (out) :: this integer, intent (in) :: s character(len=*), intent (in) :: fn, ln this%ssn = s ! store social security number this%firstname = fn this%lastname = ln end subroutine new_Alkalmazott

Megadhatunk egy személy rekordot a következőképpen:

program database use Alkalmazott type (Alkalmazott) :: szemely call new_Alkalmazott(szemely,012345678,’Paul’,’Jones’)

Konvenció szerint az első argumentum minden metódusban az osztály típusa, és rendszerint this-sel hivatkoznak rá (C++-ban) vagy self-fel (más objektum-orientált nyelvekben). A legtöbb ilyen nyelvben az első argumentum nincs explicit deklarálva, de elérhető.

A destruktor gyakran egy Alkamazott objektum törlésére szolgál. Fortran95-ben ez csak akkor szükséges, ha az Alkalmazott típusnak van mutató komponense. A mi esetünkben a destruktort az adat kinullázására használjuk:

subroutine delete_Alkalmazott(this) ! Destruktor type (Alkalmazott), intent (inout) :: this this%ssn = 0 ! nullify social security number this%firstname = ‘ ‘ this%lastname = ‘ ‘ end subroutine delete_Alkalmazott

Egy rekordot a következőképpen lehet törölni:

call delete_Alkalmazott(szemely)

Mivel az Alkalmazott típus komponensei privát adattagok, nem lehet őket közvetlenül kiiratni:

print *, szemely%ssn ! Nem működik

Helyette fel kell venni egy metódust, ami már eléri a privát adattagokat, például:

print *, getssn_Alkalmazott(szemely) ! Ez már OK. function getssn_Alkalmazott(this) result(ssn) type (Alkalmazott), intent (in) :: this integer :: ssn ssn = this%ssn ! extract the private component end function getssn_Alkalmazott

Bár talán szükségtelenül bonyolultnak tűnik a komponensek priváttá tétele, megvan az az előnye, hogy meg lehet változtatni a komponenseket anélkül, hogy az osztályt használó kódon változtatni kellene. Például tegyük fel, hogy később egy dátum adattagot szeretnénk felvenni az Alkalmazott típusba:

type Alkalmazott private integer :: ssn, age character*12 :: firstname, lastname end type Alkalmazott

Ehhez új konstruktor kell:

subroutine new_Alkalmazott_age(this,s,fn,ln,a) type (Alkalmazott), intent (out) :: this integer, intent (in) :: s, a character(len=*), intent (in) :: fn, ln this%ssn = s this%age = a this%firstname = fn this%lastname = ln end subroutine new_Alkalmazott_age

Továbbra is használhatjuk a régi konstruktort is, generikus függvények használatával. Először is átnevezzük a régi konstruktort:

subroutine new_Alkalmazott_orig(this,s,fn,ln)

Aztán definiálhatunk generikus interfészt:

interface new_Alkalmazott module procedure new_Alkalmazott_orig module procedure new_Alkalmazott_age end interface

így a new_Alkalmazott névnek most már két jelentése van.

Az összes régi kód ugyanúgy működik, mint azelőtt, és az új kód kihasználhatja az új lehetőséget:

type (Alkalmazott), dimension(2) :: szemely call new_Alkalmazott(szemely(1),012345678,’Paul’,’Jones’) call new_Alkalmazott(szemely(2),123456789,’Pat’,’Smith’,21) ... call delete_Alkalmazott(szemely(1)) ! ez első rekord törlése

Akár a print metódus működését is megváltoztathatjuk, tegyük fel, hogy azt szeretnénk, hogy egy fájlba írjon a konzol helyett. Ezt a metódust is megváltoztathatjuk ennek megfelelően.

Egy osztály nyilvános interfésze egy absztrakt típust reprezentál a külvilágnak. Azzal, hogy az osztály megköveteli a külvilágtól, hogy csak ezeket az interfészeket használja, az osztály belső részleteit privátnak tartja meg, a belső adat nem rontható el, és a metódus implementációja megváltoztatható másokkal való ütközés nélkül.

Ennek a dokumentumnak az eredeti (angol nyelvű) változata megtalálható a következő címen: http://exodus.physics.ucla.edu/Fortran95/scf95.lect5.pdf

Öröklődés

Fortran2003-ban az EXTENDS kulcsszóval lehet örökölni. A kulcsszó után zárójelbe téve következik a szülő:

type shape integer :: color logical :: filled integer :: x integer :: y end type shape type, EXTENDS ( shape ) :: rectangle integer :: length integer :: width end type rectangle type, EXTENDS ( rectangle ) :: square end type square

Az az osztály, ami nem örököl semmiből, alaptípus is egyben.

Az osztálynak megvannak azok az adattagjai, amik a szülő osztályban is megvoltak, de lehetnek új adattagjai is. Az osztály a szülő metódusait is örökli. Az öröklött adattaok többféleképpen is hozzáférhetők:

type(square) :: sq ! sq egy square objektum sq%color ! sq színe sq%rectangle%color ! sq színe a rectangle-n keresztül sq%reactangle%shape%color ! sq színe a shape-en keresztül

Fortran2003-ban az osztályok magukba foglalhatják metódusaikat. Ekkor a CONTAINS kulcsszó választja el őket az adattagoktól:

type shape integer :: color logical :: filled integer :: x integer :: y contains procedure :: initialize end type shape

ahol is a metódusok szintaktikája:

PROCEDURE [(interface-name)] [[,binding-attr-list ]::] binding-name[=> procedure-name]

ahol:

  • binding-name a metódus neve
  • binding-attr-list a PASS, NOPASS, NON_OVERRIDABLE, PUBLIC, PRIVATE és DEFERRED szavakból képezett lista
  • procedure-name az implementáló eljárás
  • ahol is a PUBLIC jelentése: nyilvános, a PRIVATE: rejtett, a NON_OVERRIDABLE: nem felülírható. A NOPASS esetén az objektum, mint a metódus argumentuma nem adódik át automatikusan, azt híváskor külön meg kell adni, míg a PASS egy másik argumentumot ad át automatikusan.

    Dinamikus kötés vagy futási idejű polimorfizmus

    A polimorfizmus azt jelenti, hogy egy adat vagy egy eljárás (függvény, szubrutin) típusa megváltozhat. Alapja az, hogy a származtatott osztály példánya a szülő osztálynak is példánya, így mindenütt használható, ahova a szülőosztály példányát várjuk. A Fortran2003-ban ez a lehetőség a CLASS kulcsszóval érhető el:

    class(shape), pointer :: sh

    Ez a pointer a későbbiekben rectangle vagy square objektumokra is mutathat, vagy bármely később definiált, a shape osztályból leszármazott objektumra.

    A polimorfizmus függvényekre és szubrutinokra is átvihető:

    subroutine setColor(sh, color) class(shape) :: sh integer :: color sh%color = color end subroutine setColor

    A polimorfizmus miatt ez a szubrutin nemcsak a shape osztály példányaira, hanem minden, a shape osztályból leszármazott osztály példányaira is hívható.

    A futás idejű típus a SELECT TYPE szerkezettel érhető el:

    subroutine initialize(sh, color, filled, x, y, length, width) ! initialize shape objects class(shape) :: sh integer :: color logical :: filled integer :: x integer :: y integer, optional :: length integer, optional :: width sh%color = color sh%filled = filled sh%x = x sh%y = y select type (sh) type is (shape) ! nem kell folytatni az inicializációt class is (rectangle) ! a rectangle vagy square specifikus inicializációja if (present(length)) then sh%length = length else sh%length = 0 endif if (present(width)) then sh%width = width else sh%width = 0 endif class default ! nem várt, nem támogatott típus: ide nem szabad eljutni stop 'initialize: unexpected type for sh object!' end select end subroutine initialize

    A Fortran2003-ról szóló rész forrása