Moduli
Ova knjiga je organizovana u celine (lekcije) da bi materija koja se izlaže bila pristupačnija za čitanje. Koncepti koji su povezni izloženi su u okviru iste lekcije, što značajno olakšava i nalaženje informacija. Iz potpuno istih razloga, u svakom programskom jeziku velike kodove želimo da podelimo u posebne datoteke (i direktorijume), grupišući povezane koncepte u celine. Na taj način prvenstveno sami sebi olakšavamo snalaženje u kodu. Haskel, kao i svaki drugi moderan programski jezik, podržava grupisanje koda u celine.
U Haskelu, modul je osnovna jedinica podele programa. Jedan modul sastoji se iz definicija funkcija, tipova i klasa tipova. Ime modula mora početi velikim slovom i može sadržati samo alfanumeričke karaktere kao i znak tačke. Modul se definiše, tako što se na početku .hs datoteke navede linija module ImeModula where.
Modul GeometrijaKruga definiše dve funkcije koje su mogu uvesti u druge module.
module GeometrijaKruga where površina :: Float -> Float površina r = r^2 * pi obim :: Float -> Float obim r = 2 * pi * r
Svaka Haskel datoteka može sadržati definiciju samo jednog modula, i obrnuto, svaki modul mora biti sadržan u jednoj datoteci. Prema tome, moduli se mogu poistovetiti sa .hs datotekama. Neophodno je imenovati .hs datoteke imenom modula (sve sa početnim velikim slovom)1.
Datoteke sa Haskel kodom možemo dalje podeliti u direktorijume. Neophodno je da sama imena modula prate hijerarhiju koja je ustanovljena direktorijumima, tako što ćemo imena direktorijuma postaviti u ime modula i razdvojiti ih tačkom. Pri tome, sve putanje se uzimaju u odnosu na korenski direktorijum projekta, tj. u odnosu na direktorijum u kom pokrećemo GHCi. Na primer, pretpostavimo da u direktorijumu projekta imamo poddirektorijum Životinje. Ako je datoteka Mačka.hs smeštena u direktorijumu Životinje, modul u datoteci Mačka.hs ćemo imenovati Životnije.Mačka. Ako se u direktorijumu Životinje nalazi direktorijum Ribe i u tom direktorijumu se nalazi datoteka Tuna.hs, tada ćemo modul u toj datoteci imenovati Životnije.Ribe.Tuna. I analogno za složenije slučajeve. Kao i datoteke, i direktorijume je neophodno imenovati sa početnim velikim slovom, koristeći samo alfanumeričke karaktere.
Izvoz definicija
Dekleracijom modula sa konstrukcijom module X where sve definicije koje se nalaze u istoj datoteci biće dostupne za uvoz u duge datoteke. Tj. na ovaj način sve definicije će biti javne.
Često ne želimo da sve definicije iz nekog modula učinimo dostupnim za uvoz u druge module, tj. želimo da neke definicije ostavimo privatnim. To se postiže tako što ćemo u dekleraciji modula, odmah nakon imena modula postaviti uređenu \(n\)-torku imena koje želimo da budu javni. Sva imena koja se ne nalaze u ovoj \(n\)-torci biće privatna. Poredak kojim su navedene imena nije važan. Naravno, ako nakon imena modula navedemo \(n\)-torku dužine \(0\), tj. (), sve definicije će biti privatne.
NAredni modul GeometrijaValjka definiše četiri funkcije funkcije, ali samo su površina i zapremina javne definicije.
module GeometrijaValjka (površina, zapremina) where površinaOsnovice :: Float -> Float površinaOsnovice r = r^2 * pi površinaOmotača :: Float -> Float -> Float površinaOmotača r h = 2 * pi * r * h površina :: Float -> Float -> Float površina r h = 2 * (površinaOsnovice r) + (površinaOmotača r h) zapremina :: Float -> Float -> Float zapremina r h = h * (površinaOsnovice r)
Neki od razloga zašto ne želimo uvek sve definicije da izvezemo su:
- Određene funkcije imaju određene pretpostavke, na primer, nisu korektne za sve argumente, ili čak nisu ni definisane za sve argumente. Ako takve funkcije učinimo privatnim, onda ćemo biti sigurni da se koriste samo u okviru modula na način na koji smo zamislili (sa nekim dodatnim proverama). Ali ovaj razlog je zapravo dublji od pukih provera argumenata. Pažljivim odabirom funkcija koje izvozimo možemo se osigurati da će funkcije biti pozvane određenim redosledom (što može biti važno ako se bavimo ulazom i izlazom u program, npr. upisivanjem u bazu)
- Privatne funkcije je lakše menjati jer se koriste na manje mesta. Karakteristična je promena tipa neke funkcije koja zahteva da se izvrše izmene na svim mestima gde se ta funkcija poziva .
- Smanjenjem broja funkcija koje su javne, olakšavamo korisnicima (kolegama programerima) naših modula razumevanje istih. Umesto da proučavaju desetine funkcija, korisnicima će biti izloženo nekoliko funkcija od suštinskog značaja.
Izvoz tipova i konstruktora
Kao i funkcije, i tipovi i konstruktori se mogu izvoziti iz modula. Konstruktori se izvoze tako što se navedu u zagradama odmah nakon tipa (u okviru iste "koordinate").
Ako iz modula Vektor želimo da izvezemo i tip Vektor i konstruktor tog tipa, možemo sledeće da napišemo
module Vektor (Vektor(MkVektor), dužina) where newtype Vektor = MkVektor (Float, Float) dužina :: Vektor -> Float dužina (Vektor (x, y)) = sqrt (a^2 + b^2)
Izvozom konstruktora, dopuštamo da se u drugim modulima konstruišu vrednosti odgovarajućeg tipa kao i da vrši dekonstrukcija podudaranjem oblika. Ako ne želimo da dozvolimo ove operacije u drugim modulima, umesto samih konstruktora možemo izvoziti funkcije koje imaju analognu ulogu ali sa dodatnom logikom (najčešće vezanu za validaciju podataka). Ova tehnika se naziva pametni konstruktor (eng. smart constructor).
U nekom programu, u modulu Ljudi podatak o osobi definisan je kao tip newtype Osoba = MkOsoba (String, Int), gde prva koordinata predstavlja ime osobe a druga koordinata predstavlja broj godina. Ako bi konstruktor MkOsoba bio javan, tada bi se bilo gde u ostatku programa mogla kreirati nova vrednost tipa Osoba. Međutim, greške se lako potkradu, i mogu se kreirati besmislene vrednosti poput Osoba ("Petar", -100). Stoga, umesto konstruktora MkOsoba svuda u programu ćemo koristiti funkciju koja ima isti tip kao i konstruktor, ali i dodatnu logiku:
napraviOsobu :: String -> Int -> Osoba napraviOsobu ime godine = MkOsoba (ime godine') where godine' = min 120 (max godine 0)
Ovu funkciju ćemo postaviti za javnu, i na taj način ćemo se osigurati da u drugim modulima ne možemo kreirati besmislene vrednosti. Takođe, ako želimo da dodamo novu validaciju (na primer za ime), biće dovoljno da dodamo tu validaciju samo na jedno mesto - u definiciji funkcije napraviOsobu.
Uvoz definicija
Dekleracije uvoza se navode odmah nakon dekleracija modula, i imaju oblik import X (...), gde su u zagradama nalazi uređena lista javnih definicija iz X koje želimo da uvezemo. Definicije koje smo uvezli možemo koristi pod istim imenom koji im dodeljen u izvornom modulu.
Ako se uvozi više različitih modula, tada se svaki uvoz navodi u posebnom redu, pri čemu poredak uvoza nije bitan. Greška pri kompilaciji (interpretaciji) će se dogoditi ako se pokuša uvoz istih imena iz različitih modula.
Da bismo uvezli funkciju obim iz modula GeometrijaKruga u modul GeometrijaValjka potrebno odmah nakom dekleracije modula a pre svih drugih definicija, napišemo liniju import GeometrijaKruga (površina). Na ovaj način funkcija obim postaje dostupna u modulu GeometrijaValjka:
module GeometrijaValjka (površina, zapremina) where import GeometrijaKruga (obim) površinaOsnovice :: Float -> Float površinaOsnovice r = r^2 * pi površinaOmotača :: Float -> Float -> Float površinaOmotača r h = (obim r) * h površina :: Float -> Float -> Float površina r h = 2 * (površinaOsnovice r) + (površinaOmotača r h) zapremina :: Float -> Float -> Float zapremina r h = h * (površinaOsnovice r)
Primetimo da je funkcija obim dostupna pod baš tim imenom, i da smo je iskoristili u funkciji površinaOmotača.
Ako se datoteke GeometrijaValjka.hs i GeometrijaKruga.hs tada je možemo da učitamo GeometrijaValjka.hs u GHCi okruženje sa :load komadom. Oba modula će se učitati:
ghci> :load GeometrijaValjka.hs [1 of 2] Compiling GeometrijaKruga ( GeometrijaKruga.hs, interpreted ) [2 of 2] Compiling GeometrijaValjka ( GeometrijaValjka.hs, interpreted ) Ok, two modules loaded.
Učitavanjem modula GeometrijaValjka.hs sve definicije (uključujući i privatne), postaju dostupne u okruženju. Iz modula GeometrijaKruga.hs biće dostupne javne definicije.
Kao i kod izvoza, sa praznom \(n\)-torkom, () nećemo uvesti ni jednu definiciju2, dok izostavljanjem bilo kakve \(n\)-torke uvozimo sve javne definicije iz modula. Takođe, ako želimo da uvezemo sve definicije osim par određenih, to možemo da uradimo sa dodajući listu "neželjenih" definicija nakon hiding ključne reči. Na primer, da uvezemo sve definicije iz modula M osim f i g, napisaćemo import M hiding (f, g).
Kvalifikovani uvoz
Dekleracije uvoza koje smo do sada videli uvoze definicije (funkcije, tipove, konstruktore...) pod istim imenom kako su originalno definisane. Međutim ako se u modul M ceo modul N koji sadrži i ime x, a u samom modulu je ime x takođe definisano, tada će ime x imati vrednost kakva je definisana u M3. Da bi se izbegao ovakav problem, koristi se kvalifikovani uvoz.
Pri kvalifikovanom uvozu, sva imena koja se uvoze, dobijaju prefiks. Podrazumevani prefiks je ime modula koji se uvozi. Dekleracija kvalifikovanog uvoza je import qualified N. Da bi se umesto imena modula koristio prefiks X potrebno je koristiti dekleraciju import qualified N as X, pri čemu ime X ne sme sadržati tačke.
Modul GeometrijaKruga možemo uvesti pod imenom GK:
module GeometrijaValjka (površina, zapremina) where import qualified GeometrijaKruga as GK površinaOsnovice :: Float -> Float površinaOsnovice = GK.površina površinaOmotača :: Float -> Float -> Float površinaOmotača r h = (GK.obim r) * h
Moduli u interaktivnom okruženju
Do sada smo učitavali datoteke u GHCi sa :load komandom. Kao što smo mogli da vidimo do sada, ako u Haskel datoteci nije deklarisan modul, tada će taj modul biti implicitno učitan pod imenom Main. Komanda :load takođe dozvoljava učitavanja više .hs datoteka od jednom, dovoljno je navesti listu datoteka razdvojenih razmakom:
ghci> :load A.hs B.hs
Komanda :load može da učita datoteke na osnovu putanje. Tako na primer, ako pokrenemo GHCi u home direktorijumu, a modul A.hs se nalazi u home/haskel-project direktorijumu, tada možemo da upišemo komandu haskel-project/A.hs da bismo učitali modul. Međutim, Alternativno, i mnogo bolje4, je da se GHCi pozicioniramo u korenski direktorijum Haskel projekta. To možemo uraditi sa :cd komandom, na primer: :cd haskel-project.
Ako upišemo komandu :load bez argumenata, tada ćemo "odbaciti" sve učitane module do tada.
Sa komandom :browse možemo prikazati listu svih definicija u poslednje učitanom modulu. Specijalno, ako nakon komande :browse navedemo ime modula, možemo pregledati sve definicije iz tog određenog modula.
Standardna biblioteka
Haskell kompajler dolazi sa mnogobrojnim modulima. I sami smo do sada koristili predefinisane funkcije za koje smo rekli da dolaze iz Prelida (Prelude), koji je zapravo samo jedan od predefinisanih modula. Modul Prelude se razlikuje po tome što se implicitno uvozi u svaki drugi modul. Taj implicitni uvoz može se sprečiti sa deklaracijom import Prelude ().
U Prelid modulu se ne nalaze same definicije, već se imena uvoze iz drugih modula i ista takva takva izvoze (takozvani reeksport). Definicije koje Prelid reeksportuje su one koje se najčešće koriste. Mnoge druge funkcije nisu dostupne kroz Prelid, već kroz posebne module. Nekih od tih modla su pojašnjeni u nastavku, a lista svih modula sa dokumentacijom se može pregledati na Haskell Hierarchical Libraries stranici.
Data.Charsadrži mnoge funkcije za proveru karaktera (isDigit,isSpace,isUpperCase...) kao i funkcije za transformacije karaktera (toUpper,toLower)Data.Complexsadrži definiciju apstraktnog tipaComplexkao i funkcije za rad sa kompleksnim brojevima. TipComplexje definisan kao apstraktan, jer time je omogućeno da se naprave posebni tipovi za različite konkretne numeričke tipove, ako na primerComplex IntiComplex Float. Konstruktor za kompleksne brojeve je:+koji se navodi između argumenata, pa tako2 :+ 3predstavlja kompleksan broj \(2 + 3i\).Data.Listsadrži mnogobrojne funkcije za rad sa listama:isPrefixOd,isSuffixOf,intersect,intercalate,lines,tail,take,takeWhile,words,zip,zip3...Data.Setsadrži definiciju apstraktnog tipaSet. Vrednost tipaSet Tza neki konkretan tipT, predstavlja skup elementa tipaA. Za razliku od lista, vrednosti u skupovima ne mogu da se ponavljaju, i njihov poredak nije bitan. Naravno, u moduluData.Setsu dostupne mnoge funkcije za rad sa skupovima, poputmember,union,cartesianProduct,size...Data.Mapsadrži definiciju apstraktnog tipaMap :: * -> * -> *. Za neke tipoveAiB, vrednost tipaMap A Bpredstavlja strukturu koja sadrži vrednosti tipaBindeksirane vrednostima tipaA(ovakva struktura se naziva i rečnik).
Da bi koristili bilo koji od navedenih modula, uglavnom je dovoljno da navedemo dekleraciju uvoza5.
Zadatak 1. Pregledati definicije Prelude modula koristeći :browse komandu.
Zadatak 2. U matematici, Mebijusova transformacija je kompleksna funkcija oblika \[m(z)=\frac{\alpha z + \beta}{\gamma z + \delta},\] gde su \(\alpha\), \(\beta\), \(\gamma\), \(\delta\) kompleksni brojevi za koje važi \(\alpha\delta-\beta\gamma \ne 0.\) Definisati modul Mebijus, i u modulu definisati funkciju novaT koja od uređene četvorke tipa (Complex Float, Complex Float, Complex Float, Complex Float) konstruiše funkciju tipa (Complex Float -> Complex Float). Na primer, novaT (1, 0, 2, 0 :+ 1) treba da vrati funkciju koja predstavlja transformaciju \(z \mapsto z/(2z+i).\) U funkciji novaT izvršiti neophodne provere, i u slučaju greške vratiti identičku transformaciju \[id(z) = z = (z + 0)/(0z + 1).\] Demonstrirati da transformacija \(t(z)=\frac{z - i}{z + i}\) preslikava realnu liniju u jedinični krug (npr. koristeći map funkciju i opseg [-10 .. 10], i funkciju abs koja računa modul kompleksnog broja).