Prvi koraci

Naredna lekcija sadrži informacije neophodne za razumevanje svih ostalih lekcija. Ako neku lekciju ne bi trebalo da preskočite, to je definitivno ova. Možda će neke stvari biti nejasne (ili dosadne) prilikom prvog čitanja, ali važno je da budu poznate.

Sekcije Let … in, Where i Prelom i nazubljivanje nisu suštinski važne za razumevanje ostatka knjige, ali će poslužiti da demonstriramo neke pojedinosti Haskel koda. Ove sekcije se mogu površno preći prilikom prvog čitanja.

Haskel kompajler

Iako je tokom istorije Haskela razvijeno nekoliko kompajlera, danas se koristi samo Glasgow Haskell Compiler (GHC). Osim kompajlera, za ozbiljnije projekte potrebno je koristiti i jedan od paketa menadžera, Stack ili Cabal.

GHC instalacija korisniku pruža dva programa. Prvi program je sam kompajler ghc koji Haskel kôd kompajlira u izvršnu datoteku. Drugi program je ghci koji se koristi kao interaktivno okruženje za interpretaciju Haskel koda. Značajan deo ove knjige biće prikazan kroz rad u GHCi okruženju (detalji će biti objašnjeni uskoro).

Razvojna okruženja za Haskel ne postoje, ali postoji Haskell Language Server (HSL) koji može da nadomesti ovaj nedostatak.

Instalacija navedenih programa može se razlikovati u zavisnosti od sistema. Trenutno se za sve operativne sisteme (Linux, Windows, Mac) preporučuje GHCup kao alat za automatsku instalaciju svih potrebnih programa. Na sajtu GHCup projekta možete pronaći instrukcije za instalaciju1. Preporučljivo je da da prilikom instalacije instalirate sve programe (Stack, Cabal, HSL).

Uz instaliran GHC potreban Vam je još samo i editor koda. Naravno, svaki editor koda može se koristiti u svrhu pisanja Haskel koda. Moja preporuka je VS Code sa instaliranim ekstenzijama Haskell i Haskell Syntax Highlighting.

Ipak, za prve korake u Haskelu nije čak ni potrebno instalirati kompajler lokalno, već je sasvim dovoljno koristiti neko od onlajn razvojnih okruženja, kao što je Replit.

Struktura Haskel koda

Haskel kôd se zapisuje u tekstualnim datotekama sa ekstenzijom .hs. Svaka .hs datoteka sastoji se od definicija kojima se imenima2 pridružuju vrednosti nekog izraza. Na primer, sledeći blok koda je validan primer .hs datoteke:

dana_u_nedelji = 7

sati_u_danu = 24
U ovoj datoteci, imenu dana_nedelji odnosno sati_u_danu je pridružena vrednost 7 odnosno 24.

Važno je znati da imena moraju početi malim slovom3. Na ostalim mestima u imenu je dozvoljeno koristiti velika i mala slova4, brojeve kao i karaktere _ i '.

Ime ne sme biti jedna od ključnih reči5 as, class, data, default, deriving, do, else, hiding if, import, in, infix, infixl, infixr, instance, let, module, newtype, of, then, type, where, qualified.

Vrednost koja je pridružena jednom imenu ne može se više menjati u toku izvršavanja (odnosno interpretacije) programa, te imena predstavljaju konstante. Samim tim, poredak definicija nije bitan.

Tip neke vrednosti predstavlja kolekciju kojoj ta vrednost pripada. Svaka vrednost pripada jednom i samo jednom tipu. Ako neka vrednost v pripada tipu T tada za tu vrednost kažemo da poseduje tip T ili da još da je tipa T i to označavamo sa v :: T. Za razliku od imena funkcije, ime tipa uvek počinje velikim slovom.

Na primer, vrednosti koje smo dodelili imenima meseci_u_godini i dana_u_nedelji poseduju tip Int. Tip Int možemo da shvatimo kao kolekciju vrednosti koja sadrži sve cele brojeve koji se mogu prezentovati na računaru.

U Haskel kodovima osim samih definicija, moguće je (i poželjno) navesti i tipove vrednosti. Dekleracija tipa ima oblik ime :: Tip. Tako bi prethodni primer sa punim dekleracijama tipova bio:

dana_u_nedelji :: Int
dana_u_nedelji = 7

sati_u_danu :: Int
sati_u_danu = 24

Iako nije neophodno da se dekleracije tipa nalaze neposredno pre definicije, u praksi se to pravilo uvek poštuje.

Izvršavanje Haskel koda

Prilikom definisanja imena u Haskel kodu, dozvoljeno je koristiti operatore i funkcije. Na primer, vrednosti tipa Int možemo sabirati, oduzimati i množiti koristeći operatore +, - i *. Koristeći ove operatore, vrednosti tipa Int, zagrade () možemo da gradimo aritmetičke izraze:

dana_u_nedelji :: Int
dana_u_nedelji = 7

sati_u_danu :: Int
sati_u_danu = 24

dana_u_godini :: Int
dana_u_godini = 28 + 4 * 30 + 7 * 31

sati_u_nedelji :: Int
sati_u_nedelji = dana_nedelji * sati_u_danu
Definicija labele sati_u_nedelji koristi definicije dana_u_nedelji i sati_u_danu. Imena uvek predstavljaju vrednosti koje su im dodeljene prilikom definicije, i mogu se koristiti umesto tih konkretnih vrednosti.

Lista definicija nije sama po sebi mnogo korisna ako ne možemo utvrditi vrednosti izraza. Tako je u prethodnom primeru zgodno saznati koliko to sati ima u nedelji, a da bi smo saznali tu vrednost potrebno je da izvršimo Haskel kôd. Postoje dva načina na koje je moguće izvršiti Haskel kôd:

  1. Kompilacijom koda. Uz pomoć GHC kompajlera, moguće je kompajlirati kôd u izvršni fajl. U ovom slučaju u kodu mora biti definisano ime main :: IO () koja služi kao početna tačka izvršavanja programa.
  2. Interpretacijom koda. Uz pomoć programa GHCi moguće je učitati definicije u interaktivno okruženje i evaluirati te definicije.

Iz određenih razloga koji će biti pojašnjeni kasnije, definisanje imena main zahteva poznavanje nešto naprednijeg programiranja u Haskelu. Stoga će demonstracija kompilacije programa biti odložena za neku narednu lekciju, a do tada ćemo koristiti isključivo GHCi program.

Rad u programu GHCi zahteva da prvo sačuvamo prethodni primer u datoteku sa ekstenzijom .hs. Na primer, neka je to datoteka prviProgram.hs. Zatim je potrebno da otvorimo terminal i promenimo aktivni direktorijum tog terminala u direktorijum u kom se nalazi datoteka prviProgram.hs. Nakon toga možemo pokrenuti ghci program6. Nakon pokretanja komande ghci komande pojaviće se tekst poput sledećeg:

C:\Users\User\Desktop>ghci
GHCi, version 9.0.2: https://www.haskell.org/ghc/  :? for help
ghci>
Izgled terminala nakon pokretanja GHCi programa na Windows operativnom sistemu.

Pokretanjem ghci programa otvara se novi prompt7 u koji možemo da upišemo Haskel kôd ili GHCi naredbe. GHCi naredbe je moguće koristiti samo unutar GHCi okruženja, i imena tih naredbi počinju sa :. Jedna od tih naredbi je i :load uz pomoć koje možemo da učitamo sadržaj neke .hs datoteke.

Na primer, učitavanje datoteke prviProgram.hs izgleda ovako:

ghci> :load prviProgram.hs
[1 of 1] Compiling Main             ( prviProgram.hs, interpreted )
Ok, one module loaded.
ghci>
Ako niste pokrenuli ghci iz direktorijuma u kom se nalazi datoteka prviProgram.hs dobićete grešku error: can't find file: prviProgram.hs. U tom slučaju, možete u naredni prompt navesti putanju do željene datoteke :l putanja\do\prviProgram.hs

Nakon što je datoteka prviProgram.hs učitana, možemo koristiti definisane vrednosti. Ako upišemo dana_u_nedelji i pritisnemo Enter, vrednost dodeljena ovom imenu će biti ispisana, i nakon toga će pojaviti prompt spreman za novi unos:

ghci> dana_u_nedelji
7
ghci>

Mnogo bitnije je ono što dobijamo nakon unosa imena sati_u_nedelji. U ovom slučaju, GHCi će interpretirati Haskel kôd dana_nedelji * sati_u_nedelji odnosno iskoristiće definicije navedenih vrednosti a zatim i izvršiti naznačeno množenje. Rezultat će biti ispisan:

ghci> sati_u_nedelji
168

Možda opisani postupak deluje trivijalno, ali predstavlja suštinu rada u interaktivnom okruženju8.

Ako želimo, u datoteku prviProgram.hs možemo dodati još definicija poput sekundi_u_danu = 24 * 60 * 60 itd... Da bismo mogli da koristimo nove definicije, neophodno je da sačuvamo izmene datoteke prviProgram.hs a zatim da ponovo učitamo datoteku u GHCi. To možemo da učinimo ponovo sa naredbom :load prviProgram.hs ili sa naredbom :reload koja ponovo učitava sve datoteke koje su do tada bile učitane:

ghci> :reload
Ok, one module loaded.
ghci> sekundi_u_danu
86400
Naredba :reload je pogotovu korisna kada se u GHCi učita više datoteka od jednom. O tome nismo govorili, ali je moguće i to učiniti uz opreznost da se nazivi labela ne ponavljaju.

U GHCi prompt je moguće direktno unositi i Haskel izraze koji će se odmah evaluirati:

ghci> 10
10
ghci> 10 * 10 + 1
101
Prvi korisnički unos nije mogao biti dodatno evaluiran, te je stoga ispisan isti nazad. Drugi unos je sveden na jedinstven broj i taj broj je ispisan nazad.

Zapravo u GHCi prompt je moguće uneti i definicije. Ove definicije će važiti samo dok je trenutna sesija aktivna i neće biti upisane ni u jednu datoteku. Važno je znati da je u GHCi okruženju dozvoljeno predefinisati već postojeće definicije. Tom prilikom, ime će predstavljati onaj izraz koji mu je dodeljen poslednjom definicijom, a sve prethodna definicija će biti izgubljena. Komandom :reload sve definicije napisane u GHCi okruženju se odbacuju.

ghci> a = 100 + 10
ghci> a
110
ghci> a = 200 + 10
ghci> a
210
ghci> :reload
Ok, one module loaded.
ghci> a
<interactive>:1:1: error: Variable not in scope: a
Prilikom definisanja nekog imena, vrednost dodeljenog izraza se neće ispisati u terminal. Tek unosom imena u prompt ispisujemo vrednost.

Za izlazak iz GHCi programa, koristi se naredba :quit.

Let ... in

Prilikom pisanja izraza neretko se javlja potreba da neke vrednosti privremeno dodelimo novom imenu. Upravo nam to omogućuje let in konstrukcija.

Na primer, želeli bismo da napišemo izraz kojim računamo površinu kvadra visine 10 širine 5 i dužine 3. Koristeći formulu \(P=2(ab + bc + ca)\) dobijamo kôd:

površina :: Int
površina = 2*(10*5 + 5*3 + 3*10)

Navedena definicija je malo nepregledna. Takođe, ako želimo da izračunamo zapreminu nekog drugog kvadra, svaku od dimenzija moramo da promenimo na dva mesta što predstavlja potencijalni izvor greške. Zbog toga ćemo iskoristiti let in sintaksu da bismo lokalno definisali imena a, b i c9:

površina :: Int
površina = let a = 10; b = 5; c = 3 in 2*(a*b + b*c + c*a)

Dakle, sa let … in konstrukcijom moguće je lokalno definisati jedno ili više imena koja će biti dostupna samo u nekom izrazu. Opšti oblik let … in izraza sastoji se od niza definicija koje su razdvojene znakom ;, i nakon čega sledi i izraz u kom želimo da te definicije budu dostupne:

let ime1 = izraz1; …; imeN = izrazN in izraz

Naravno, imena definisana u let … in konstrukciji dostupna su samo unutar te let … in konstrukcije.

Napominjemo da let … in predstavlja jedan izraz, i da let … in sintaksa nije nužno vezana za definicije. Zbog toga GHCi uspešno pronalazi vrednost izraza let a = 2 in a + 3:

ghci> let a = 2 in a + 3
5

Samim tim, moguće je ugnježdavati let … in izraze:

ghci> let a = 2 in (let b = 3 in a + b)
5
Navedeni Haskel izraz je ekvivalentan izrazu let a = 2; b = 3 in a + b.

Where

Sa where sintaksom takođe je moguće uvesti lokalne definicije kao i sa let … in sintaksom. Za razliku od let in konstrukcije koja predstavlja izraz, where sintaksa se isključivo koristi uz definicije. Opšti oblik where sintakse je:

ime = izraz where ime1 = izraz1; …; imeN = izrazN

Primer sa površinom kvadra iz prethodne sekcije bi mogao i ovako da se napiše:

površina :: Int
površina = 2*(a*b + b*c + c*a) where a = 10; b = 5; c = 3 

Za razliku let in sintakse, sintaksa where se ne može koristiti van definicija:

ghci> broj = a + 3 where a = 3
ghci> broj
5
ghci> a + 3 where a = 3
<interactive>:1:7: error: parse error on input ‘where’
Kôd a + 3 where a = 3 nije validan izraz, zbog čega je došlo do sintaksne greške.

Važno je znati da je u okviru let in ili where sintakse moguće lokalno predefinisati neka već definisana imena. Zbog toga, u narednom primeru, ime b predstavlja vrednost 11 a ne 21:

a :: Int
a = 20

b :: Int
b = a + 1 where a = 10
ghci> b
11

Prelom i nazubljivanje

Često nije zgodno pisati ceo izraz u jednoj liniji. U tom slučaju, možemo prelomiti linije poštujući pravila o nazubljivanju10 Haskel koda. Kako su opisi tih pravila dosta tehnički, ovde navodimo neformalna pravila koja dobro sažimaju ta tehnička pravila:

  1. Nazubljivanje Haskel koda se vrši isključivo sa razmacima.
  2. Početak definicije ne sme da bude nazubljen.
  3. Svaki izraz se može prelomiti na proizvoljnom razmaku. Ostatak prelomljenog izraza mora da bude nazubljen više nego linija u kojoj počinje izraz koji sadrži taj kod.
  4. Niz definicija u let ... in i where sintaksi može se prelomiti u više linija. Tada, početak svake definicije mora da bude postavljen tačno ispod početka prve definicije.

Što se tiče prvog pravila, ono se odnosi na polemiku između razmaka i tabova. Iako će kompajler često iskompajlirati kôd koji sadrži tabove, pri kompajliranju će se javiti upozorenja.

Drugo pravilo je takođe jednostavno, i njim se zabranjuje nazubljivanje samog početka neke definicije:

dobraDefinicija = 1

  lošaDefinicija = 1

Treće pravilo je pomalo neobično za jezike koji su osetljivi na nazubljivanje11. Po ovom pravili svaki izraz je moguće prelomiti. Na primer, sledeća tri koda su primeri validnog preloma istog izraza:

a = 10 * 10 + 1
a = 10 * 10
  + 1
a = 10 *
        10
  + 1
Broj razmaka nije bitan pri nazubljivanju. Ako neka linija sadrži barem jedan više razmak na početku nego druga, onda ćemo smatrati da je prva linija više nazubljena od druge.

Naredni kôd nije dobro formatiran, jer linija + 1 nije dodatno nazubljena u odnosu na početak a =:

a =
    10 * 10
+ 1

Kako i let … in sintaksa predstavlja izraz, i taj izraz možemo prelomiti:

površina :: Int
površina = let a = 10; b = 5; c = 3
    in 2*(a*b + b*c + c*a)
površina :: Int
površina = let
              a = 10; b = 5; c = 3
           in
              2*(a*b + b*c + c*a)
Oba primera su dobro nazubljena

Takođe, i where sintaksu je moguće prelomiti:

površina :: Int
površina = 2*(a*b + b*c + c*a)
    where a = 10; b = 5; c = 3 
površina :: Int
površina = 2*(a*b + b*c + c*a)
    where
        a = 10; b = 5; c = 3 
Oba primera su dobro nazubljena

Četvrto pravilo se odnosi na niz definicija u let ... in i where sintaksama. Po ovom pravilu, niz je moguće prelomiti u više linija, pri čemu se znaci ; mogu izostaviti. Pri prelomu sve definicije se moraju postaviti tačno jedna ispod druge. Naredni kodovi su dobro nazubljeni:

površina :: Int
površina = let
            a = 10
            b = 5
            c = 3
           in 2*(a*b + b*c + c*a)
površina :: Int
površina = let a = 10
               b = 5
               c = 3
           in 2*(a*b + b*c + c*a)
površina :: Int
površina = 2*(a*b + b*c + c*a)
    where
        a = 10
        b = 5
        c = 3 
površina :: Int
površina = 2*(a*b + b*c + c*a)
    where a = 10
          b = 5
          c = 3 

Zapravo, ovakvi blokovi definicija se mogu shvatiti kao male .hs datoteke. I kao što u .hs datoteci svaka definicija mora da počne od leve ivice, tako u bloku definicija sve definicije moraju da počnu od iste kolone12 (a ta kolona je uspostavljena početkom prve definicije).

w :: Int
w = 2 * v
    where
        v :: Int
        v = 10 * 3
Osim definicija, u bloku je moguće navesti i dekleracije tipova, ali se to retko radi u praksi.

Komentari

Kao i svi drugi programskim jezici, Haskel podržava pisanje komentara odnosno delova koda koji ne utiču na izvršavanje. U Haskelu postoje dve vrste komentara:

  1. Linijski komentari jesu komentari koji se nalaze samo u jednoj liniji koda. Linijski komentari počinju sa -- i obuhvataju (desni) ostatak linije. Linijski komentari se mogu postaviti i u linijama u kojima se nalazi kôd.
  2. Višelinijski komentari jesu komentari koji pružaju kroz jednu ili više linija koda. Ovi komentari se nalaze između {- i -}
-- ovo je komentar

a = 2 * 10 + 1 -- i ovo je komentar

{-
Ovo
je
takođe
komentar
-}
Primer komentara u .hs datoteci.