Funktsionaalse ja loogilise programmeerimise keeled
"Tavalised" programmeerimiskeeled (C, C++. Java, Pascal, Fortran, Ada jne) on imperatiivsed e. käsukeeled - programmeerija määrab samm-sammult, mida peab tegema, sageli tegeleb isegi muutujate paigutamisega arvuti mälus (C-s, Ada-s jne). Kuid näiteks tabelarvutuse leht on ka programm, kuid selle kirjutaja ei määra, millises järjekorras tabeli lahtritesse kirjutatud valemid peab täitma - järjekorra määrab tabelarvutussüsteem; programmeerija ei tea, kuidas süsteem muutujad (tabeli lahtritesse kirjutatud arvud) mällu paigutab jne.
Ka funktsionaalsetes programmeerimiskeeltes ei määra programmeerija, milliseid samme ja mis järjekorras arvuti peab sooritama. Programmeerija kirjeldab vaid, milliseid funktsioone tuleb arvutada - funktsionaalprogrammeerimise keeltes on kogu programm üks suur funktsioon. Programmeerija määrab vaid, mida peab arvutama; kuidas seda teha, on translaatori/arvuti asi.
Näitena Haskel-is kirjutatud massiivi järjestamise programm (quicksort-meetodil):
qsort [] = []
qsort (x:xs) = qsort less_x ++ [x] ++ qsort greater_x
where less_x =
filter (<x) xs
greater_x = = filter (>=x) xs
Sellest programmist on lihtne aru saada ka siis, kui Haskel-i
süntaksit ei tunta:
nimistu x:xs
(x - nimistu esimene element e.
"pea", xs - kõik ülejäänud
elemendid e. "saba") järjestamiseks jagame nimistu esimesele elemendile
x järgnevad elemendid kahte ossa:
esimesest elemendist x väiksemad less_x
ja esimesest elemendist suuremad greater_x,
järjestame need osad (qsort less_x,
qsort greater_x) ja ühendame
(operatsioon ++) siis järjestatud
osad, lisades keskele elemendi x;
osade leidmisel kasutatav funktsioon filter on Haskel-i primitiiv, mis
moodustab uue nimistu, mis koosneb esimese argumendina esitatud
tingimust rahuldavatest elementidest teise argumendina esitatud
nimistust ("filtreerib" tingimusega nimistu).
Veel lakoonilisem on Haskel-is Fibbonacci arvude arvutamise programm:
fibo n = calc n 1 1 where- s.t. n-nda Fibbonacci arvu leidmiseks käivitame rekursiivselt kirjeldatud kolmekohalise funktsiooni calc, andes selle argumentideks n 1 1 - esimene argument näitab, mitmendat Fibbonacci arvu otsitakse, teine-kolmas on Fibbonacci arvude rea kaks viimasena leitud liiget. Definitsiooni kohaselt calc 1 1 1 = 1, calc 2 1 1 = calc 1 2 1 = 2 jne; käivitades väljastab programm 40-nda Fibbonacci arvu:
calc k f1 f2 = if k<2 then f1 else calc (k-1) (f1+f2) f1
-- funktsioon main käivitab programmi:
main = print(show(40) ++ "-s Fibonacci arv is " ++ show(fibo 40))
"40-s Fibonacci arv is 165580141"
Ka loogilise programmeerimise keeltes (tuntuim neist on Prolog - Programming in Logic) vaid kirjeldatakse ülesanne ja ülesandes osalevate suuruste vahelised seosed, kuid ei määrata, kuidas ülesannet peab lahendama. Selles sarnanevad loogilise programmeerimise keeled funktsionaalsete keelega, kuid nad lähevad veel kaugemale arvutist ja lähemale inimesele, s.t. loomulikule keelele. Prologis ei ole andmetüüpide kontrolli ja siin võib ühes nimistus esineda erinevat tüüpi suurusi, näiteks isiku aadress võib olla kirjeldatud nimistuna ["Paldiski mnt", 157] - Haskel-is erinevat tüüpi andmete (string, täisarv) esinemine samas nimistus pole lubatud.
Loogilise programmeerimise keelte tähtsaim erinevus funktsionaalsetest keeltest on see, et loogilise programmeerimise keeles programmeerija ei kirjelda funktsioone (s.t. kirjeldatud avaldised ei tagasta näit matemaatilisi väärtusi), vaid predikaate - avaldistel on vaid tõeväärtus (true, false), sellepärast tuleb lisatakse tagastatav väärtus (näiteks järjestatud massiiv, mille ülalesitatud Haskel-i programmis funktsioon qsort tagastab, predikaadi argumendiks - allpoolesitatud Prologi predikaat järjesta vaid testib, et selle teine argument on esimeseks argumendiks oleva nimistu järjestatud kuju. Näitena Prolog-is kirjutatud massiivi järjestamise programm (quicksort-meetodil):
järjesta([],[]).
järjesta([X|Tail],Järjestatud):-
järjesta(Tail,Tail1),
paiguta(X,Tail1,Järjestatud).
paiguta(X,[],[X]).
paiguta(X,[Y|Tail],[X,Y|Tail]):-
X =< Y,!.
paiguta(X,[Y|Tail],[Y|Tail1]):-
paiguta(X,Tail,Tail1).
Selgitusi:
- tühi nimistu ei muutu (rida 1)
- kui järjestatava nimistu esimene element on X ja ülejäänud elemendid moodustavad
alamnimistu Tail (saba; sellisel
kujul esitatud nimistut tähistatakse [X|Tail],
rida 2), siis järjestame alamnimistu Tail
ja paigutame elemendi X õigele
kohale järjestamise järel saadud (järjestatud !) nimistus Tail1 :
- elemendi X
paigutamisel tühja nimistusse saame üheelemendilise nimistu (rida 5);
- kui X on väiksem kui
järjestamise järel saadud nimistu esimene element Y, siis element X lihtsalt lisatakse uue nimistu ette,
seega lisamisel saadud nimistu esimesed elemendid on X,Y (rida 6; hüüumärk ! ütleb, et muid
juhte pole tarvis enam vaadelda);
- muul juhul (kui X ei ole
väiksem kui Y) jääb Y uue nimistu esimeseks elemendiks ja
talle järgnevad elemendid saadakse elemendi X paigutamisel elemendile Y järgnevate elementide (järjestatud)
nimistusse Tail.
Ülaltoodud Haskel-is esitatud Fibbonacci arvude arvutamise programmi "tõlkimine" Prolog-i on veel otsesem - Haskel-is väljastatavad funktsiooni väärtused on vaid lisatud Prolog-i predikaatide viimaseks argumendiks:
fibo(N,F):-- Prologis koma , tõlgendatakse kui loogiline "ja" (AND, konjunktsioon) ja semikoolon ; kui loogiline "või" (OR, disjunktsioon); kuna predikaadi argumentidena (enamuses Prologi realisatsioonidest) ei või kasutada aritmeetilisi avaldisi, peab need eraldi real välja arvutama; lõige ! (cut) tingimuse K<2 järel ütleb, et muid võimalusi pole tarvis enam uurida; selle võiks ära jätta, kui predikaadi calc kirjelduses panna teise alternatiivi algusesse (; järele) täpsustav tingimus K >=2 ). Prolog-is (tavaliselt) pole väljakutsutavat funktsiooni main, Prologi interpretaatoris võib käivitada ükskõik millise programmis kirjeldatud predikaadi (s.t. kontrollida, milliste muutujate väärtuste korral see on tõene), sellepärast käivitame predikaadi fibo(40,F); järgnevas ?- on Prologi viip (prompt), selle järel predikaadi kirjutamisel (kõik predikaadid lõpevad punktiga!) annab Prolog vastuse:
calc(N,1,1,F),
print(N), print('-s Fibbonacci arv on '), print(F), nl.
calc(K,F1,F2,F):-
K<2,!,F=F1;
K1 is K-1,
F0 is F1+F2,
calc(K1,F0,F1,F).
?- fibo(40,F).
40-s Fibbonacci arv on 165580141
Funktsionalsete ja loogilise programmeerimise keelete tase on märksa kõrgem kui imperatiivsetel keeltel, siin tegeldakse mõistetega, mis on lähemal loomulikule keelele. Funktsionalsed ja loogilise programmeerimise keeled ei tegele probleemi esitamisega arvuti mälus, sellepärast pole neis üldse imperatiivsete keelte põhikonstruktsiooni: omistamist. Reaalse maailma probleeme lahendades me ei omista muutujatele väärtusi - meid huvitavad suhted reaalse maailma suuruste vahel ja funktsioonid, mis neid suurusi teisendavad. Sellepärast puudub funktsionalsetes ja loogilise programmeerimise keeletes imperatiivse programmeerimise pahe, mis on seotud muutujatele väärtuste omistamise ja sellega programmi olekute teisendamisega: kõrvalmõjud (side-effects). Mingi alamprotseduur või funktsioon võib muuta muutujat, mida kasutab mingi teine avaldis, kuid selle muutuse toimima hakkamise hetke pole mõnikord üldse võimalik määrata, see sõltub translaatorist ja võib olla ükskord nii, teinekord naa. Kui (näiteks) funktsiooni f(x) kehas (definitsioonis) muudetakse globaalset muutujat a , siis milline tuleb avaldise a + f(b) väärtus - kas siin kasutatakse muutuja a "vana" väärtust või "uut" - seda, mis a -le omistati funktsiooni f(b) arvutamise ajal? Paljudes programmeerimiskeeltes on sellised kõrvalmõjud suurte probleemide põhjustajad, sest a muutmine võib toimuda kaugel kohast, kus esineb kahemõtteline avaldis a + f(b).
Kuid isegi samas avaldises toimuvate arvutuste (muutujatele väärtuste omistamiste) mõju ei ole mõnikord võimalik kontrollida. Näiteks C-s avaldise i/++i (i on täisarv, defineeritud int i; ) väärtus võib olla (sõltuvalt translaatorist) kas 0 või 1 ja omistamise a = b++ + b++ tulemus pole üldse määratud.
Kuna funktsionaalsed ja loogilised programmeerimiskeeled on palju lähemal loomulikule keelele kui imperatiivsed keeled, on ka neis kirjutatud programmid tavaliselt mitu korda lühemad kui vastav programm imperatiivsetes keeltes; neist on palju lihtsam aru saada ka siis, kui vastava keele süntaksit ei tunta, sest nende loogika on lähedane loomulikus keeles kasutatavale loogikale (vt ülalesitatud Haskel-is ja Prolog-is esitatud järjestamisprogrammide kommentaare).
Võrdlusena on järgnevas sama quicksort-meetodil massiivi järjestamise programm C-s - siin peab tegelema muutujate tüüpide, massiivikirjelduste ja muuga, mis on seotud andmete esitamisega arvuti mälus:
qsort( a, lo, hi ) int a[], hi, lo;
{
int h, l, p, t;
if (lo < hi) {
l = lo;
h = hi;
p = a[hi];
do { while ((l < h) && (a[l]
<= p))
l = l+1;
while ((h > l) && (a[h] >= p))
h = h-1;
if (l < h) {
t = a[l];
a[l] = a[h];
a[h] = t;
} } while (l < h);
t = a[l];
a[l] = a[hi];
a[hi] = t;
qsort( a, lo, l-1 );
qsort( a, l+1, hi );
}
}
Kindlasti tekib nende võrdlemiste järel küsimus - miks siis funktsionaalsed ja loogilised programmeerimiskeeled pole (seni) veel üldlevinud, miks ikkagi enamus programmeerimisfirmasid kasutab imperatiivseid keeli (C, Java jne)?
Põhjusi on mitu, kuid tähtsaim kindlasti on erinevus probleemist arusaamise tasemes. Funktsionaalsetes ja loogilise programmeerimise keeltes programmeerimisse kuulub suur osa probleemi konseptuaalsest analüüsist - mis on teada/antud, mida on tarvis leida/saavutada, millised on probleemivaldkonna (DoD - Domain of Discourse) elementide/suuruste vahelised suhted jne. Seda arusaamist tavaliselt ei saa tavaprogrammeerijalt üldse nõuda, selleks peab olema kas vastava valdkonna spetsialist või on probleemi selgitatud (pikkade nõupidamiste ajal) programmeerimisfirma osakonna/projektijuhile; nende nõupidamiste tulemusena valmib ülesande spetsifikatsioon, mis on juba oluliselt arvutilähedasem kirjeldus. Selle põhjal saab (pärast ülesande jagamist väiksemateks alamosadeks) koostada konkreetsed programmeerimisülesanded tavaprogrammeerijatele realiseerimiseks C-s, C++-s, Java-s jne; programmeerimisfirmale esitatud reaalse maailma ülesandest ja selle lahendamismeetodist ei pea nad üldse eriti palju teadma - see pole nende tase. Nende tase on tsüklid jooksma ja viidad õigesse kohta osutama saada.
Eelnevast peaks olema ka arusaadav, milleks kõrge tasemega keeli (funktsionaalsid ja loogilise programmeerimise keeli) vajatakse ja kasutatakse. Neid vajatakse ja neid kasutatakse ennekõike probleemi konseptuaalseks uurimiseks ja erinevate kontseptuaalsete lahenduste testimiseks - kas olemasolevate andmete põhjal on võimalik leida vajalikke suurusi, kas on olemas kõik vajalik info, kui kiiresti hakkavad esitatud lahendusmeetodid töötama jne. Probleemidele "piisavalt kõrgelt" vaatamine, s.t. kõigi probleemiga seotud suhete ja muutujate leidmine on suur kunst ja see saavutatakse vaid pikkaajalise harjutamise käigus. Inimesed saavad väga halvasti aru ajas toimuvatest suhetest, kuid programmeerimine on ennekõike just "tuleviku genereerimine" - programmerija peab suutma ette näha, mida programmi töö ajal hakkab juhtuma. Loogilise ja funktsionaalse programmeerimise keeled on väga hea vahend selle "tuleviku ettenägemise" kunsti õppimiseks, sest nende kasutamisel on minimaalne probleemi arvutis esitamisega seotud (teisejärguliste) detailide mõju. Sellepärast kuulub tutvumine loogilise ja funktsionaalprogrammeerimise keeltega (peaaegu) kõigisse elukutselisi infotöötlejaid valmistavasse akadeemilisse õpeprogrammi.
Funktsionaalse ja loogilise programmeerimise keeltes lahendatavad ülesanded (ka õpeprogrammi harjutusülesanded) on enamasti märksa kõrgema tasemega kui käsukeelte harjutusülesanded, sest nendesse kuulub olulise osana probleemi konseptuaalne analüüs. Näiteks 2005 a funktsionaalsete keelte programmeerimisvõistluse ICFP 2005 Programming Contest (ICFP - International Conference on Functional Programming) ülesanne põhines mängul: robot-pangaröövel püüab leida kõik tema teele jäävad pangad ja need siis tühjaks teha, robot-politseiniku ülesanne on teda takistada. Kõik osavõtvad meeskonnad pidid programmeerima nii pangaröövli kui ka politseiniku ja tulemust hinnati programme omavahel Chicago virtuaalses Hyde Park-is võistlema lastes.
Kuid funktsionaalsetes ja loogilise programmeerimise keeli ei kasutata ainult "mängu"- (õpe)-ülesannete lahendamiseks. Neis on programmeeritud kõnetuvastussüsteeme, loomulikust keelest arusaamise süsteeme, translaatorite genereerimise süsteeme (väga loomulik ülesanne selliste keelte jaoks), semantilise web-i töövahendeid (XML, RDF jne analüsaatoreid/editore), mänge jne. Kuid nagu eespool öeldud, nende kasutamine nõuab ka programmeerijalt rohkem kui näiteks C- programmeerimine, sest siin programm on suures osas ka probleemi spetsifikatsioon; sageli nimetataksegi selliseid programme "täidetavateks spetsifikatsioonideks".
Ja kuna töö toimub palju kõrgemal tasemel, toob funktsionaal- ja loogilise programmeerimise keelte kasutamine tavaliselt kaasa tunduva programmeerijate tööjõudluse tõusu. Rootsi elektroonikafirma Ericsson leidis testides, et telefonivõrkude tarkvara kirjutavate programmeerijate tööviljakus tõusis Haskell-i kasutamisel 9-25%; pärast seda loodi Ericsson-is oma funktsionaalprogrammeerimise keel Erlang ja Ericsson-i andmetel on selle kasutamine kaasa toonud tunduva programmeerijate tööviljakuse tõusu.